<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet type="text/xsl" href="/pretty-feed-v3.xsl"?>
<rss
  version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:source="https://source.scripting.com/"
>
  <channel>
    <title>Paul Tibbetts</title>
    <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/tags/code/</link>
    <description>Recent posts by Paul Tibbetts on code</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <lastBuildDate>Wed, 04 Mar 2026 15:27:40 +0000</lastBuildDate>
    <atom:link href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/tags/code/feed.xml" rel="self" type="application/rss+xml" /><item>
      <title>I Made a Terraform Provider for Mythic Beasts</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/03/i-made-a-terraform-provider-for-mythic-beasts/</link>
      <pubDate>Tue, 03 Mar 2026 12:00:17 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/03/i-made-a-terraform-provider-for-mythic-beasts/</guid>
      <category>Terraform</category>
      <category>Mythic Beasts</category>
      <category>go</category>
      <category>code</category>
      <description>paultibbetts/terraform-provider-mythicbeasts is a Terraform provider that lets you declare infrastructure from the company Mythic Beasts as code.
I use it to provision the infrastructure for my personal website.
This is why I built it, what it does, and how to use it.</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/paultibbetts/terraform-provider-mythicbeasts">paultibbetts/terraform-provider-mythicbeasts</a>
 is a Terraform provider that lets you declare infrastructure from the company <a href="https://www.mythic-beasts.com/">Mythic Beasts</a>
 as code.</p>
<p>I use it to provision the infrastructure for my personal website.</p>
<p>This is why I built it, what it does, and how to use it.</p>
<h2 id="why-i-built-it">Why I built it</h2>
<p>I prefer writing down my infrastructure as code. It&rsquo;s something I&rsquo;ve been doing for years now, and I don&rsquo;t think I could go back to doing things manually, no matter how small or simple the setup.</p>
<p>I&rsquo;d decided to use a Pi from Mythic Beasts to host my site and saw that they had APIs available, but no Terraform provider.</p>
<p>Terraform is a tool I&rsquo;ve been using for infrastructure for a while now, and I wanted to use it with Mythic Beasts because:</p>
<ul>
<li>I wanted a fully reproducible setup</li>
<li>I didn&rsquo;t want to click around the control panel</li>
<li>their Pis are IPv6-only, so I&rsquo;d need to also set up the proxy</li>
<li>
<ul>
<li>I wanted to declare the Pi and the endpoints for the proxy in the same place</li>
</ul>
</li>
</ul>
<p>Plus, I&rsquo;d never written a Terraform provider before, and I wanted to see what it was like.</p>
<h2 id="what-it-does">What it does</h2>
<p>Terraform is a tool for writing your infrastructure as code, and a provider is a plugin that lets it work with different infrastructure providers.</p>
<p>For example you can write:</p>
<pre><code class="language-hcl">terraform {
  required_version = &#34;&gt;= 1.11.0&#34;

  required_providers {
    mythicbeasts = {
      source = &#34;paultibbetts/mythicbeasts&#34;
    }
  }
}

resource &#34;mythicbeasts_pi&#34; &#34;pi&#34; {
	identifier   = &#34;web&#34;
	disk_size    = 10
	model        = 4
	memory       = 4096
	os_image     = &#34;rpi-bookworm-arm64&#34;
    ssh_key      = &#34;ssh-ed25519 ...&#34;
    wait_for_dns = true
}</code></pre>
<p>then run <code>terraform apply</code>, and you would get a Raspberry Pi 4 from Mythic Beasts.</p>
<h3 id="resources">Resources</h3>
<p>The provider uses the Mythic Beasts APIs to configure the following resources:</p>
<ul>
<li>VPS</li>
<li>Raspberry Pi</li>
<li>Proxy endpoint</li>
</ul>
<p>The Raspberry Pis from Mythic Beasts are on an IPv6-only network, so the Proxy endpoints are to allow users trying to access from an IPv4-only network.</p>
<h3 id="data-sources">Data sources</h3>
<p>Some of the settings for the resources require specific values, like the <code>os_image</code> to use, and data sources make it easy to get this info from the API.</p>
<p>For example, the <code>mythicbeasts_pi_operating_systems</code> data source can be used to get all valid <code>os_image</code> values for a Raspberry Pi 4:</p>
<pre><code class="language-hcl">data &#34;mythicbeasts_pi_operating_systems&#34; &#34;four&#34; {
  model = 4
}</code></pre>
<p>where you would inspect the data to choose the right value to use.</p>
<p>Alternatively you could use Terraform to find the right value for you, for example to use &ldquo;Bookworm&rdquo; for &ldquo;Arm64&rdquo;:</p>
<pre><code class="language-hcl">data &#34;mythicbeasts_pi_operating_systems&#34; &#34;four&#34; {
  model = 4
}

locals {
  bookworm_arm64 = one([
    for image in data.mythicbeasts_pi_operating_systems.four.images :
    image.id
    if strcontains(image.id, &#34;bookworm&#34;) &amp;&amp;
    strcontains(image.id, &#34;arm64&#34;)
  ])
}

resource &#34;mythicbeasts_pi&#34; &#34;bookworm&#34; {
  model    = 4
  os_image = local.bookworm_arm64
  ...
}</code></pre>
<p>where Terraform will find the image with both &ldquo;bookworm&rdquo; and &ldquo;arm64&rdquo; in the ID.</p>
<h3 id="attributes">Attributes</h3>
<p>Using Terraform for your infrastructure not only lets you write it down as code, with all the <a href="https://infra.paultibbetts.uk/decisions/why-iac/">benefits that brings</a>
, but you can use the attributes of one thing as inputs for another.</p>
<p>For example: the Pis from Mythic Beasts are on an IPv6-only network and need endpoints set up on the proxy to let users access them from IPv4-only networks.</p>
<p>To do this you can get the attribute from a Pi, like the IP address, and then use that as the input for the proxy endpoint resource:</p>
<pre><code class="language-hcl">locals {
  domain = &#34;example.com&#34;
  subdomains = {
    apex  = &#34;@&#34;
    www   = &#34;www&#34;
  }
}

resource &#34;mythicbeasts_pi&#34; &#34;web&#34; {
	identifier   = &#34;web&#34;
	disk_size    = 10
	model        = 4
	memory       = 4096
	os_image     = &#34;rpi-bookworm-arm64&#34;
    ssh_key      = &#34;ssh-ed25519 ...&#34;
    wait_for_dns = true
}

resource &#34;mythicbeasts_proxy_endpoint&#34; &#34;endpoint&#34; {
  for_each = local.subdomains

  domain         = local.domain
  hostname       = each.value
  address        = mythicbeasts_pi.web.ip
  site           = &#34;all&#34;
  proxy_protocol = true
}</code></pre>
<h2 id="how-to-use-it">How to use it</h2>
<p>The provider is available on <a href="https://registry.terraform.io/providers/paultibbetts/mythicbeasts/">the Terraform registry</a>
.</p>
<h3 id="authentication">Authentication</h3>
<p>The Mythic Beasts APIs require an <a href="https://www.mythic-beasts.com/customer/api-users">API key</a>
 for authentication.</p>
<p>When creating the key you must add permissions to work with the APIs you wish to use, such as:</p>
<ul>
<li>&ldquo;Virtual Server Provisioning&rdquo; for VPS</li>
<li>&ldquo;Raspberry Pi provisioning&rdquo; for Pis</li>
<li>&ldquo;IPv4 to IPv6 Proxy API&rdquo; for Proxy endpoints</li>
</ul>
<h3 id="proxy-endpoint-domain-management">Proxy endpoint domain management</h3>
<p>The domain used for proxy endpoints must be registered with the Mythic Beasts control panel.</p>
<p>This can be done by registering the domain using their <a href="https://www.mythic-beasts.com/customer/domains">domain management</a>
 or by <a href="https://www.mythic-beasts.com/customer/3rdpartydomain">adding it as a 3rd party domain</a>
.</p>
<h3 id="installation">Installation</h3>
<pre><code class="language-hcl">terraform {
  required_version = &#34;&gt;= 1.11.0&#34;

  required_providers {
    mythicbeasts = {
      source = &#34;paultibbetts/mythicbeasts&#34;
    }
  }
}

provider &#34;mythicbeasts&#34; {
  keyid  = &#34;your-keyid&#34;
  secret = &#34;your-secret&#34;
}</code></pre>
<p>Alternatively, credentials can be supplied via environment variables:</p>
<ul>
<li><code>MYTHICBEASTS_KEYID</code></li>
<li><code>MYTHICBEASTS_SECRET</code></li>
</ul>
<h2 id="examples">Examples</h2>
<p>Examples of each resource and data source can be found at <a href="https://github.com/paultibbetts/terraform-provider-mythicbeasts/blob/main/examples">/examples</a>
.</p>
<p>These are also used to generate the <a href="https://registry.terraform.io/providers/paultibbetts/mythicbeasts/latest/docs">documentation hosted by the Terraform registry</a>
.</p>
<h2 id="how-it-works">How it works</h2>
<p>The provider uses the newer <a href="https://github.com/hashicorp/terraform-plugin-framework">Terraform Plugin Framework</a>
.</p>
<p>For it to work with the Mythic Beasts APIs I had to create a Go client, which can be found <a href="https://github.com/paultibbetts/mythicbeasts-client-go/">on GitHub</a>
. The client handles the APIs and means that the provider only needs to focus on Terraform itself.</p>
<p>The Mythic Beasts VPS API needs values that are only ever used when creating a VPS and so the provider uses the new <a href="https://developer.hashicorp.com/terraform/language/manage-sensitive-data/write-only">write-only</a>
 arguments feature, which is only available in Terraform v1.11.0 and later. Write-only is normally used for sensitive values like passwords but the VPS API doesn&rsquo;t return every attribute so write-only is used for these values when creating the VPS and then they are forgotten.</p>
<h2 id="support">Support</h2>
<p>To make it clear, this is an unofficial provider that is not endorsed by Mythic Beasts.</p>
<p>It&rsquo;s currently at version <code>v0.1</code> and I wouldn&rsquo;t consider it stable until it hits <code>v1.0</code>.</p>
<p>Right now it supports the APIs I need it to. Further features will be added as needed.</p>
<h2 id="in-use">In use</h2>
<p>I use the provider to provision a Raspberry Pi 4 from Mythic Beasts and set up the proxy endpoints for my personal website.</p>
<p>The whole setup, with Terraform to provision the Pi and then Ansible to configure it, is documented at <a href="https://infra.paultibbetts.uk">infra.paultibbetts.uk</a>
.</p>
<h2 id="whats-next">What&rsquo;s next?</h2>
<p>The provider works with the VPS, Pi, and Proxy APIs, as these are what I currently use or have used in the past.</p>
<p>Mythic Beasts also have a <a href="https://www.mythic-beasts.com/support/api/dnsv2">DNS API</a>
 which lets you manage the DNS records for domains.</p>
<p>If I were to add this to the provider I could change the management of my domain from Cloudflare over to Mythic Beasts, which would improve my website&rsquo;s <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/19/moved-my-website-from-github-pages-to-a-raspberry-pi/">sovereignty</a>
.</p>]]></content:encoded><source:markdown>
[paultibbetts/terraform-provider-mythicbeasts](https://github.com/paultibbetts/terraform-provider-mythicbeasts) is a Terraform provider that lets you declare infrastructure from the company [Mythic Beasts](https://www.mythic-beasts.com/) as code.

I use it to provision the infrastructure for my personal website.

This is why I built it, what it does, and how to use it.

&lt;!--more--&gt;

## Why I built it

I prefer writing down my infrastructure as code. It&#39;s something I&#39;ve been doing for years now, and I don&#39;t think I could go back to doing things manually, no matter how small or simple the setup.

I&#39;d decided to use a Pi from Mythic Beasts to host my site and saw that they had APIs available, but no Terraform provider.

Terraform is a tool I&#39;ve been using for infrastructure for a while now, and I wanted to use it with Mythic Beasts because:

- I wanted a fully reproducible setup
- I didn&#39;t want to click around the control panel
- their Pis are IPv6-only, so I&#39;d need to also set up the proxy
- - I wanted to declare the Pi and the endpoints for the proxy in the same place

Plus, I&#39;d never written a Terraform provider before, and I wanted to see what it was like.

## What it does

Terraform is a tool for writing your infrastructure as code, and a provider is a plugin that lets it work with different infrastructure providers.

For example you can write:

```hcl
terraform {
  required_version = &#34;&gt;= 1.11.0&#34;

  required_providers {
    mythicbeasts = {
      source = &#34;paultibbetts/mythicbeasts&#34;
    }
  }
}

resource &#34;mythicbeasts_pi&#34; &#34;pi&#34; {
	identifier   = &#34;web&#34;
	disk_size    = 10
	model        = 4
	memory       = 4096
	os_image     = &#34;rpi-bookworm-arm64&#34;
    ssh_key      = &#34;ssh-ed25519 ...&#34;
    wait_for_dns = true
}
```

then run `terraform apply`, and you would get a Raspberry Pi 4 from Mythic Beasts.

### Resources

The provider uses the Mythic Beasts APIs to configure the following resources:

- VPS
- Raspberry Pi
- Proxy endpoint

The Raspberry Pis from Mythic Beasts are on an IPv6-only network, so the Proxy endpoints are to allow users trying to access from an IPv4-only network.

### Data sources

Some of the settings for the resources require specific values, like the `os_image` to use, and data sources make it easy to get this info from the API.

For example, the `mythicbeasts_pi_operating_systems` data source can be used to get all valid `os_image` values for a Raspberry Pi 4:

```hcl
data &#34;mythicbeasts_pi_operating_systems&#34; &#34;four&#34; {
  model = 4
}
```

where you would inspect the data to choose the right value to use.

Alternatively you could use Terraform to find the right value for you, for example to use &#34;Bookworm&#34; for &#34;Arm64&#34;:

```hcl
data &#34;mythicbeasts_pi_operating_systems&#34; &#34;four&#34; {
  model = 4
}

locals {
  bookworm_arm64 = one([
    for image in data.mythicbeasts_pi_operating_systems.four.images :
    image.id
    if strcontains(image.id, &#34;bookworm&#34;) &amp;&amp;
    strcontains(image.id, &#34;arm64&#34;)
  ])
}

resource &#34;mythicbeasts_pi&#34; &#34;bookworm&#34; {
  model    = 4
  os_image = local.bookworm_arm64
  ...
}
```

where Terraform will find the image with both &#34;bookworm&#34; and &#34;arm64&#34; in the ID.

### Attributes

Using Terraform for your infrastructure not only lets you write it down as code, with all the [benefits that brings](https://infra.paultibbetts.uk/decisions/why-iac/), but you can use the attributes of one thing as inputs for another.

For example: the Pis from Mythic Beasts are on an IPv6-only network and need endpoints set up on the proxy to let users access them from IPv4-only networks.

To do this you can get the attribute from a Pi, like the IP address, and then use that as the input for the proxy endpoint resource:

```hcl
locals {
  domain = &#34;example.com&#34;
  subdomains = {
    apex  = &#34;@&#34;
    www   = &#34;www&#34;
  }
}

resource &#34;mythicbeasts_pi&#34; &#34;web&#34; {
	identifier   = &#34;web&#34;
	disk_size    = 10
	model        = 4
	memory       = 4096
	os_image     = &#34;rpi-bookworm-arm64&#34;
    ssh_key      = &#34;ssh-ed25519 ...&#34;
    wait_for_dns = true
}

resource &#34;mythicbeasts_proxy_endpoint&#34; &#34;endpoint&#34; {
  for_each = local.subdomains

  domain         = local.domain
  hostname       = each.value
  address        = mythicbeasts_pi.web.ip
  site           = &#34;all&#34;
  proxy_protocol = true
}
```

## How to use it

The provider is available on [the Terraform registry](https://registry.terraform.io/providers/paultibbetts/mythicbeasts/).

### Authentication

The Mythic Beasts APIs require an [API key](https://www.mythic-beasts.com/customer/api-users) for authentication.

When creating the key you must add permissions to work with the APIs you wish to use, such as:

- &#34;Virtual Server Provisioning&#34; for VPS
- &#34;Raspberry Pi provisioning&#34; for Pis
- &#34;IPv4 to IPv6 Proxy API&#34; for Proxy endpoints

### Proxy endpoint domain management

The domain used for proxy endpoints must be registered with the Mythic Beasts control panel.

This can be done by registering the domain using their [domain management](https://www.mythic-beasts.com/customer/domains) or by [adding it as a 3rd party domain](https://www.mythic-beasts.com/customer/3rdpartydomain).

### Installation

```hcl
terraform {
  required_version = &#34;&gt;= 1.11.0&#34;

  required_providers {
    mythicbeasts = {
      source = &#34;paultibbetts/mythicbeasts&#34;
    }
  }
}

provider &#34;mythicbeasts&#34; {
  keyid  = &#34;your-keyid&#34;
  secret = &#34;your-secret&#34;
}
```

Alternatively, credentials can be supplied via environment variables:

- `MYTHICBEASTS_KEYID`
- `MYTHICBEASTS_SECRET`

## Examples

Examples of each resource and data source can be found at [/examples](https://github.com/paultibbetts/terraform-provider-mythicbeasts/blob/main/examples).

These are also used to generate the [documentation hosted by the Terraform registry](https://registry.terraform.io/providers/paultibbetts/mythicbeasts/latest/docs).

## How it works

The provider uses the newer [Terraform Plugin Framework](https://github.com/hashicorp/terraform-plugin-framework).

For it to work with the Mythic Beasts APIs I had to create a Go client, which can be found [on GitHub](https://github.com/paultibbetts/mythicbeasts-client-go/). The client handles the APIs and means that the provider only needs to focus on Terraform itself.

The Mythic Beasts VPS API needs values that are only ever used when creating a VPS and so the provider uses the new [write-only](https://developer.hashicorp.com/terraform/language/manage-sensitive-data/write-only) arguments feature, which is only available in Terraform v1.11.0 and later. Write-only is normally used for sensitive values like passwords but the VPS API doesn&#39;t return every attribute so write-only is used for these values when creating the VPS and then they are forgotten.

## Support

To make it clear, this is an unofficial provider that is not endorsed by Mythic Beasts.

It&#39;s currently at version `v0.1` and I wouldn&#39;t consider it stable until it hits `v1.0`.

Right now it supports the APIs I need it to. Further features will be added as needed.

## In use

I use the provider to provision a Raspberry Pi 4 from Mythic Beasts and set up the proxy endpoints for my personal website.

The whole setup, with Terraform to provision the Pi and then Ansible to configure it, is documented at [infra.paultibbetts.uk](https://infra.paultibbetts.uk).

## What&#39;s next?

The provider works with the VPS, Pi, and Proxy APIs, as these are what I currently use or have used in the past.

Mythic Beasts also have a [DNS API](https://www.mythic-beasts.com/support/api/dnsv2) which lets you manage the DNS records for domains.

If I were to add this to the provider I could change the management of my domain from Cloudflare over to Mythic Beasts, which would improve my website&#39;s [sovereignty](/2026/02/19/moved-my-website-from-github-pages-to-a-raspberry-pi/).
</source:markdown></item><item>
      <title>I Made an Ansible Role for Caddy</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/24/i-made-an-ansible-role-for-caddy/</link>
      <pubDate>Tue, 24 Feb 2026 12:10:45 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/24/i-made-an-ansible-role-for-caddy/</guid>
      <category>Ansible</category>
      <category>Caddy</category>
      <category>code</category>
      <description>paultibbetts/ansible-role-caddy is an Ansible role for Caddy web server that lets you install Caddy with plugins.
I use it for my personal site and in my homelab.
This is what it does and how to use it.</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/paultibbetts/ansible-role-caddy">paultibbetts/ansible-role-caddy</a>
 is an Ansible role for Caddy web server that lets you install Caddy with plugins.</p>
<p>I use it for my personal site and in my homelab.</p>
<p>This is what it does and how to use it.</p>
<h2 id="what-it-does">What it does</h2>
<p>An Ansible role is like a plugin that extends Ansible with the ability to do something, and in this case it&rsquo;s to install and manage <a href="https://caddyserver.com/">Caddy</a>
.</p>
<p>Specifically it:</p>
<ul>
<li>installs Caddy</li>
<li>
<ul>
<li>defaults to using the <code>apt</code> package manager</li>
</ul>
</li>
<li>
<ul>
<li>or it can be given the URL to a specific binary</li>
</ul>
</li>
<li>
<ul>
<li>or it can be given a list of plugins</li>
</ul>
</li>
<li>can be used to manage Caddy</li>
<li>
<ul>
<li>or completely uninstall it</li>
</ul>
</li>
<li>can write the Caddyfile from a template</li>
</ul>
<h2 id="why-caddy">Why Caddy?</h2>
<p>I use Caddy because it manages TLS certificates automatically and, with a plugin, supports <a href="https://letsencrypt.org/docs/challenge-types/#dns-01-challenge">DNS-01 challenges</a>
.</p>
<p>DNS-01 is useful when services aren&rsquo;t reachable by Lets Encrypt, like in a homelab, so private networks can also have TLS.</p>
<h2 id="design-goals">Design goals</h2>
<p>The role was designed with the following in mind:</p>
<ul>
<li>idempotent and safe to re-run</li>
<li>Debian and Ubuntu</li>
<li>works on x86 and ARM</li>
<li>supports installing with plugins</li>
<li>can optionally write the Caddyfile</li>
</ul>
<p>There are already Ansible roles for Caddy, like the one listed in <a href="https://caddyserver.com/docs/install#ansible">the docs</a>
, but I needed a role that could install Caddy with plugins.</p>
<h2 id="how-to-use-it">How to use it</h2>
<p>The role is available on <a href="https://galaxy.ansible.com/ui/standalone/roles/paultibbetts/caddy/">Ansible Galaxy</a>
.</p>
<h3 id="install">Install</h3>
<p>You can install it by adding:</p>
<pre><code class="language-yaml">- name: paultibbetts.caddy
  version: 1.0.0</code></pre>
<p>to your <code>requirements.yml</code> file and then running:</p>
<pre><code class="language-sh">ansible-galaxy role install -r requirements.yml</code></pre>
<h3 id="variables">Variables</h3>
<p>The variables for the role, as well as the default settings, can all be overridden.</p>
<p>The most important ones are the install method, the plugins to include, and whether the role writes the Caddyfile for you.</p>
<pre><code class="language-yaml">caddy_install_method: apt</code></pre>
<p>The role defaults to installing Caddy via <code>apt</code>, the package manager for Debian/Ubuntu.</p>
<p>This means Caddy does not include any plugins but it does get updated when you run <code>apt upgrade</code>.</p>
<pre><code class="language-yaml">caddy_install_method: download</code></pre>
<p>Alternatively you can change the install method to <code>download</code>.</p>
<p>Now you can pass in the URL of a specific binary using:</p>
<pre><code class="language-yaml">caddy_download_url: &#34;&#34;</code></pre>
<p>Using this you could build a binary for Caddy (including plugins), host it, then pass that URL to the role and that specific binary will be installed.</p>
<p>This is currently the only way to install a specific version of Caddy that isn&rsquo;t &ldquo;latest&rdquo;.</p>
<p>If you want to install plugins for Caddy and don&rsquo;t want to build your own binary you can leave the download URL empty and pass in the plugins:</p>
<pre><code class="language-yaml">caddy_plugins:
  - github.com/caddy-dns/cloudflare</code></pre>
<p>and the role will generate a URL from the <a href="https://caddyserver.com/download">download page</a>
.</p>
<pre><code class="language-yaml">caddy_caddyfile_template: &#34;&#34; | &#34;{{ playbook_dir }}/templates/Caddyfile.j2&#34;</code></pre>
<p>The Caddyfile template variable lets you pass in a template of a Caddyfile for Caddy to use and it will be written to the appropriate place.</p>
<p>Letting the role write the Caddyfile means you can import the role, set its variables, and have all of Caddy configured by one thing - you don&rsquo;t need to write extra tasks after installation to get it up and running.</p>
<h3 id="examples">Examples</h3>
<p>The following examples show how the role can be used for a public website and for a private homelab reverse proxy.</p>
<p>In both instances Caddy acquires TLS certificates from Lets Encrypt, using Cloudflare to do DNS-01 challenges.</p>
<h4 id="example-personal-website">Example: Personal website</h4>
<p>The role can be used to setup Caddy to host a personal website.</p>
<pre><code class="language-yaml">- hosts: server
  become: true
  roles:
    - role: paultibbetts.caddy
      vars:
        caddy_caddyfile_template: &#34;{{ playbook_dir }}/templates/Caddyfile.j2&#34;
        caddy_install_method: download
        caddy_plugins:
          - github.com/caddy-dns/cloudflare
        caddy_manage_systemd_env_file: true
        caddy_systemd_env:
          CF_API_TOKEN: &#34;{{ vault_cf_api_token }}&#34;</code></pre>
<p>The <code>download</code> install method is used to install Caddy with the <a href="https://github.com/caddy-dns/cloudflare">Cloudflare plugin</a>
, the Cloudflare API token is passed to Caddy from Ansible vault, and a template is provided for the Caddyfile, which could look like:</p>
<pre><code class="language-Caddyfile">(cf_tls) {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
}

{% for site in web_sites %}
{{ site }} {
  import cf_tls
  root * {{ web_sites_root }}/{{ site }}/current
  file_server
  encode zstd gzip
  log
}
{% endfor %}</code></pre>
<p>This sets up Caddy to use Cloudflare to do DNS-01 challenges with Lets Encrypt to get a TLS certificate for each <code>site in web_sites</code>.</p>
<h4 id="example-homelab">Example: Homelab</h4>
<p>The role can also be used to setup Caddy in a private network, like a homelab.</p>
<pre><code class="language-yaml">- hosts: homelab
  become: true
  roles:
    - role: paultibbetts.caddy
      vars:
        caddy_caddyfile_template: &#34;{{ playbook_dir }}/templates/Caddyfile.j2&#34;
        caddy_install_method: download
        caddy_plugins:
          - github.com/caddy-dns/cloudflare
        caddy_manage_systemd_env_file: true
        caddy_systemd_env:
          CF_API_TOKEN: &#34;{{ vault_cf_api_token }}&#34;</code></pre>
<pre><code class="language-Caddyfile">{
        acme_dns cloudflare {env.CF_API_TOKEN}
}

service.homelab.example {
        reverse_proxy &lt;ip&gt;:&lt;port&gt;
}</code></pre>
<p>where the Cloudflare plugin is configured once and is then used to do DNS-01 challenges for any TLS certificates Caddy requests. Note this style has been deprecated and you should do it per host, like the earlier example for a personal website.</p>
<p>DNS-01 challenges are solved by writing a TXT record to prove domain ownership. This means Lets Encrypt is able to issue certificates for domains it can&rsquo;t reach, which makes DNS-01 ideal when Caddy is running in a homelab.</p>
<h2 id="molecule-scenarios">Molecule Scenarios</h2>
<p>Ansible Molecule is a way to run tests for an Ansible role.</p>
<p>The role includes molecule scenarios that test it works on x86 as well as Arm, because I use the role on a Raspberry Pi 4, as well as scenarios for the Cloudflare plugin and the Caddyfile templating.</p>
<h2 id="in-use">In use</h2>
<p>I use this role as part of the code that sets up the infrastructure for my website.</p>
<p>Terraform is used to provision the server, then Ansible is used to configure it, with this role setting up Caddy and the Caddyfile it uses.</p>
<p>The complete setup - Terraform, Ansible, Caddy, and the rest of the files - is documented at <a href="https://infra.paultibbetts.uk">infra.paultibbetts.uk</a>
, where I explain how it all fits together.</p>]]></content:encoded><source:markdown>
[paultibbetts/ansible-role-caddy](https://github.com/paultibbetts/ansible-role-caddy) is an Ansible role for Caddy web server that lets you install Caddy with plugins.

I use it for my personal site and in my homelab.

This is what it does and how to use it.

&lt;!--more--&gt;

## What it does

An Ansible role is like a plugin that extends Ansible with the ability to do something, and in this case it&#39;s to install and manage [Caddy](https://caddyserver.com/).

Specifically it:

- installs Caddy
- - defaults to using the `apt` package manager
- - or it can be given the URL to a specific binary
- - or it can be given a list of plugins
- can be used to manage Caddy
- - or completely uninstall it
- can write the Caddyfile from a template

## Why Caddy?

I use Caddy because it manages TLS certificates automatically and, with a plugin, supports [DNS-01 challenges](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge).

DNS-01 is useful when services aren&#39;t reachable by Lets Encrypt, like in a homelab, so private networks can also have TLS.

## Design goals

The role was designed with the following in mind:

- idempotent and safe to re-run
- Debian and Ubuntu
- works on x86 and ARM
- supports installing with plugins
- can optionally write the Caddyfile

There are already Ansible roles for Caddy, like the one listed in [the docs](https://caddyserver.com/docs/install#ansible), but I needed a role that could install Caddy with plugins.

## How to use it

The role is available on [Ansible Galaxy](https://galaxy.ansible.com/ui/standalone/roles/paultibbetts/caddy/).

### Install

You can install it by adding:

```yaml
- name: paultibbetts.caddy
  version: 1.0.0
```

to your `requirements.yml` file and then running:

```sh
ansible-galaxy role install -r requirements.yml
```

### Variables

The variables for the role, as well as the default settings, can all be overridden.

The most important ones are the install method, the plugins to include, and whether the role writes the Caddyfile for you.

```yaml
caddy_install_method: apt
```

The role defaults to installing Caddy via `apt`, the package manager for Debian/Ubuntu.

This means Caddy does not include any plugins but it does get updated when you run `apt upgrade`.

```yaml
caddy_install_method: download
```

Alternatively you can change the install method to `download`.

Now you can pass in the URL of a specific binary using:

```yaml
caddy_download_url: &#34;&#34;
```

Using this you could build a binary for Caddy (including plugins), host it, then pass that URL to the role and that specific binary will be installed.

This is currently the only way to install a specific version of Caddy that isn&#39;t &#34;latest&#34;.

If you want to install plugins for Caddy and don&#39;t want to build your own binary you can leave the download URL empty and pass in the plugins:

```yaml
caddy_plugins:
  - github.com/caddy-dns/cloudflare
```

and the role will generate a URL from the [download page](https://caddyserver.com/download).

```yaml
caddy_caddyfile_template: &#34;&#34; | &#34;{{ playbook_dir }}/templates/Caddyfile.j2&#34;
```

The Caddyfile template variable lets you pass in a template of a Caddyfile for Caddy to use and it will be written to the appropriate place.

Letting the role write the Caddyfile means you can import the role, set its variables, and have all of Caddy configured by one thing - you don&#39;t need to write extra tasks after installation to get it up and running.

### Examples

The following examples show how the role can be used for a public website and for a private homelab reverse proxy.

In both instances Caddy acquires TLS certificates from Lets Encrypt, using Cloudflare to do DNS-01 challenges.

#### Example: Personal website

The role can be used to setup Caddy to host a personal website.

```yaml
- hosts: server
  become: true
  roles:
    - role: paultibbetts.caddy
      vars:
        caddy_caddyfile_template: &#34;{{ playbook_dir }}/templates/Caddyfile.j2&#34;
        caddy_install_method: download
        caddy_plugins:
          - github.com/caddy-dns/cloudflare
        caddy_manage_systemd_env_file: true
        caddy_systemd_env:
          CF_API_TOKEN: &#34;{{ vault_cf_api_token }}&#34;
```

The `download` install method is used to install Caddy with the [Cloudflare plugin](https://github.com/caddy-dns/cloudflare), the Cloudflare API token is passed to Caddy from Ansible vault, and a template is provided for the Caddyfile, which could look like:

```Caddyfile
(cf_tls) {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
}

{% for site in web_sites %}
{{ site }} {
  import cf_tls
  root * {{ web_sites_root }}/{{ site }}/current
  file_server
  encode zstd gzip
  log
}
{% endfor %}
```

This sets up Caddy to use Cloudflare to do DNS-01 challenges with Lets Encrypt to get a TLS certificate for each `site in web_sites`.

#### Example: Homelab

The role can also be used to setup Caddy in a private network, like a homelab.

```yaml
- hosts: homelab
  become: true
  roles:
    - role: paultibbetts.caddy
      vars:
        caddy_caddyfile_template: &#34;{{ playbook_dir }}/templates/Caddyfile.j2&#34;
        caddy_install_method: download
        caddy_plugins:
          - github.com/caddy-dns/cloudflare
        caddy_manage_systemd_env_file: true
        caddy_systemd_env:
          CF_API_TOKEN: &#34;{{ vault_cf_api_token }}&#34;
```

```Caddyfile
{
        acme_dns cloudflare {env.CF_API_TOKEN}
}

service.homelab.example {
        reverse_proxy &lt;ip&gt;:&lt;port&gt;
}
```

where the Cloudflare plugin is configured once and is then used to do DNS-01 challenges for any TLS certificates Caddy requests. Note this style has been deprecated and you should do it per host, like the earlier example for a personal website.

DNS-01 challenges are solved by writing a TXT record to prove domain ownership. This means Lets Encrypt is able to issue certificates for domains it can&#39;t reach, which makes DNS-01 ideal when Caddy is running in a homelab.

## Molecule Scenarios

Ansible Molecule is a way to run tests for an Ansible role.

The role includes molecule scenarios that test it works on x86 as well as Arm, because I use the role on a Raspberry Pi 4, as well as scenarios for the Cloudflare plugin and the Caddyfile templating.

## In use

I use this role as part of the code that sets up the infrastructure for my website.

Terraform is used to provision the server, then Ansible is used to configure it, with this role setting up Caddy and the Caddyfile it uses.

The complete setup - Terraform, Ansible, Caddy, and the rest of the files - is documented at [infra.paultibbetts.uk](https://infra.paultibbetts.uk), where I explain how it all fits together.
</source:markdown></item><item>
      <title>Published: paultibbetts/terraform-provider-mythicbeasts A Terraform provider for …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/09/2026-02-09-151500/</link>
      <pubDate>Mon, 09 Feb 2026 15:15:00 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/09/2026-02-09-151500/</guid>
      <category>Terraform</category>
      <category>go</category>
      <category>code</category>
      <category>Mythic Beasts</category>
      <description>Published: paultibbetts/terraform-provider-mythicbeasts A Terraform provider for Mythic Beasts.</description>
      <content:encoded><![CDATA[<p>Published: <a href="https://github.com/paultibbetts/terraform-provider-mythicbeasts">paultibbetts/terraform-provider-mythicbeasts</a>
</p>
<p>A Terraform provider for Mythic Beasts.</p>]]></content:encoded><source:markdown>
Published: [paultibbetts/terraform-provider-mythicbeasts](https://github.com/paultibbetts/terraform-provider-mythicbeasts)

A Terraform provider for Mythic Beasts.
</source:markdown></item><item>
      <title>Published: paultibbetts/mythic-beasts-client-go A client for the Mythic Beasts …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/06/2026-02-06-130000/</link>
      <pubDate>Fri, 06 Feb 2026 13:00:00 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/06/2026-02-06-130000/</guid>
      <category>go</category>
      <category>code</category>
      <category>Mythic Beasts</category>
      <description>Published: paultibbetts/mythic-beasts-client-go A client for the Mythic Beasts APIs, written in Go.</description>
      <content:encoded><![CDATA[<p>Published: <a href="https://github.com/paultibbetts/mythicbeasts-client-go/">paultibbetts/mythic-beasts-client-go</a>
</p>
<p>A client for the Mythic Beasts APIs, written in Go.</p>]]></content:encoded><source:markdown>
Published: [paultibbetts/mythic-beasts-client-go](https://github.com/paultibbetts/mythicbeasts-client-go/)

A client for the Mythic Beasts APIs, written in Go.
</source:markdown></item><item>
      <title>Published: paultibbetts/ansible-role-caddy An Ansible role to install and …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/03/2026-02-03-175500/</link>
      <pubDate>Tue, 03 Feb 2026 17:55:00 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/03/2026-02-03-175500/</guid>
      <category>Ansible</category>
      <category>code</category>
      <category>Caddy</category>
      <description>Published: paultibbetts/ansible-role-caddy An Ansible role to install and operate Caddy web server.</description>
      <content:encoded><![CDATA[<p>Published: <a href="https://github.com/paultibbetts/ansible-role-caddy">paultibbetts/ansible-role-caddy</a>
</p>
<p>An Ansible role to install and operate Caddy web server.</p>]]></content:encoded><source:markdown>
Published: [paultibbetts/ansible-role-caddy](https://github.com/paultibbetts/ansible-role-caddy)

An Ansible role to install and operate Caddy web server.
</source:markdown></item>
  </channel>
</rss>
