Articles

Automating TLS Certificates in a Homelab: Smallstep PKI, Caddy, and Drone CI/CD — A Static Website Case Study

Automating TLS Certificates in a Homelab: Smallstep PKI, Caddy, and Drone CI/CD — A Static Website Case Study

Abstract

Running services in a homelab behind an internal reverse proxy creates a certificate management problem that is easy to underestimate. This article walks through a real deployment: a Hugo static website built and deployed by Drone CI/CD, with UAT served by an nginx container behind Caddy, and production hosted on Firebase. The core challenge is not just issuing certificates — it is ensuring that Docker containers running as part of the CI/CD pipeline actually trust the internal CA. We examine how Smallstep step-ca acts as the homelab root of trust, how Caddy automates certificate provisioning via ACME, and how an Ansible role bootstraps CA trust on the Docker host so that the Drone Server container can communicate with Gitea over HTTPS. The infrastructure provisioning side (via OpenTofu and Ansible) is also described, with an honest account of what IaC actually created versus what was pre-existing.


Introduction

A typical homelab runs multiple interdependent services: a code repository (Gitea), a CI/CD platform (Drone), a reverse proxy (Caddy), and application workloads. When these services must communicate over HTTPS, certificate management becomes a persistent problem.

Self-signed certificates trigger browser warnings and require manual distribution to every client. Let’s Encrypt is designed for Internet-facing services, not internal infrastructure. Manual certificate creation and distribution is tedious and breaks when certificates expire and nobody notices.

This article describes a concrete setup built around a static website:

  • Source code in a self-hosted Gitea repository
  • Push to develop triggers a Drone CI/CD pipeline
  • Drone builds the Hugo site and deploys to UAT on an internal nginx container
  • UAT is served via HTTPS through a pre-existing Caddy reverse proxy
  • Push to main builds and deploys to Firebase Hosting (production)
  • The Docker host and Drone infrastructure were provisioned via Infrastructure-as-Code (OpenTofu and Ansible)

The certificate problem turned out to be the most interesting part of this setup — not because issuing certificates is hard, but because Docker containers do not automatically trust your internal CA, and making them do so requires understanding the full trust chain.

What you will learn:

  • Why Docker containers do not inherit the host’s CA trust, and what to do about it
  • The bootstrap chicken-and-egg problem when installing an internal CA cert
  • How Drone Server, Drone Runner, and ephemeral build containers each have different certificate trust requirements
  • How Caddy automates certificate provisioning via ACME against Smallstep step-ca
  • What OpenTofu and Ansible actually provisioned in this setup (and what was pre-existing)
  • How the UAT and production deployment pipelines work

Prerequisites:

Familiarity with Docker, Git workflows, CI/CD concepts, and a general understanding of TLS certificates. Knowledge of Smallstep, Caddy, or Drone is not required — each is explained as it appears.


Understanding the Components

Before examining the certificate infrastructure, it helps to know what each piece does.

Gitea: Self-Hosted Git Repository

Gitea is a lightweight, self-hosted Git platform — GitHub but on your own hardware. It stores the Hugo source code, the Drone pipeline definition (.drone.yml), and infrastructure code. When you push a branch, Gitea sends a webhook to Drone to start a pipeline.

Smallstep step-ca: Internal Certificate Authority

step-ca is a certificate authority designed to be simple and container-native. It issues and signs TLS certificates, implements the ACME protocol (the same one Let’s Encrypt uses), and supports short-lived certificates to contain the damage from key compromise.

In this setup, step-ca is the single root of trust for all internal services. Everything that needs to trust an internal HTTPS endpoint ultimately traces back to the step-ca root certificate.

Caddy: Reverse Proxy and ACME Client

Caddy is a reverse proxy with a built-in ACME client. Unlike nginx, which requires manual or scripted certificate management, Caddy requests a certificate from step-ca, installs it, and renews it automatically before expiry — with no manual intervention.

In this setup, Caddy handles TLS termination for all internal services, including git.<your-domain>, ci.<your-domain>, and uat.<your-domain>.

Drone: CI/CD Pipeline Orchestrator

Drone is a container-native CI/CD platform. When Gitea sends a webhook, Drone reads .drone.yml and executes each pipeline step in a Docker container. For this Hugo website, the pipeline builds the static site and deploys it — to UAT via rsync over SSH, and to Firebase for production.

OpenTofu and Ansible: Infrastructure as Code

OpenTofu (an open-source Terraform fork) provisions infrastructure — virtual machines, networks, storage. Ansible configures the software running on those machines. In this setup, they work together: OpenTofu creates the QEMU VM (<pipeline-host>), Ansible installs Docker and deploys the services.

Firebase Hosting: Production Deployment

Firebase Hosting is Google’s managed static site hosting. It handles HTTPS automatically using public CA certificates managed by Google. The production pipeline requires no internal certificate management — Drone builds the site and uploads it to Firebase via the Firebase CLI.


The Full Architecture

Proxmox Hypervisor
🖥
<app-host>
Docker host
📦
Gitea
Git Server
🌐
Caddy
Reverse Proxy / TLS termination
<b>🖥</b> &lt;pipeline-host&gt;
🔄
Drone Server
CI/CD orchestrator
🔄
Drone Runner
Executes pipeline steps
🌐
nginx UAT
Serves Hugo static files
🔐
step-ca
Root CA / ACME
OpenTofu + Ansible
IaC provisioning
ACME cert issuanceCA bootstrap (Ansible)provisions
💻
Developer
git push
👥
Internet Users
HTTPS browse
☁️
Firebase Hosting
Production (public CA)
git pushwebhookrsync / SSH deployHTTPS requestproxy :8080firebase deploy
Text is not SVG - cannot display
Full homelab architecture showing certificate flows (vermillion) and request flows (sumi ink). The IaC provisioned the pipeline host and its containers. Smallstep step-ca and Caddy were pre-existing.

The workflow:

  1. Developer pushes code to develop in Gitea
  2. Gitea sends a webhook to Drone Server
  3. Drone Runner executes the pipeline:
    • Builds the Hugo site in an ephemeral container
    • Deploys to UAT via rsync over SSH to the nginx container on <pipeline-host>
  4. Caddy (pre-existing, the reverse proxy container) proxies HTTPS requests to the nginx container
    • Caddy’s certificate for uat.<your-domain> was issued by Smallstep step-ca via ACME
  5. Push to main triggers a separate pipeline step that deploys to Firebase Hosting

Provisioning the Pipeline Infrastructure: OpenTofu and Ansible

It is important to be precise about what IaC actually did here, and what it did not do.

What Was Pre-Existing

The following services were already running before the IaC ran:

  • Smallstep step-ca (the CA VM) — set up manually, pre-existing root CA for the homelab
  • Caddy Reverse Proxy (the reverse proxy container) — set up via Proxmox helper scripts, pre-existing
  • Gitea (running on another Docker host, <app-host>) — pre-existing
  • Pihole DNS — pre-existing internal DNS resolver

These are prerequisites for the pipeline infrastructure, not outputs of it. The IaC assumed they existed.

What OpenTofu Provisioned

The compute module in OpenTofu created one new QEMU virtual machine: <pipeline-host>, a Docker host on Proxmox. This is the host where Drone Server, Drone Runner, and the nginx UAT server would run.

# Simplified: OpenTofu compute module
resource "proxmox_vm_qemu" "pipeline_host" {
  name   = "<pipeline-host>"
  vmid   = <ID>
  clone  = "ubuntu-2204-template"
  cores  = 2
  memory = 4096
  # ... disk, network config
}

No VMs for step-ca, Caddy, or Gitea were created by this IaC. They already existed.

What Ansible Provisioned

Four Ansible roles ran against <pipeline-host>:

  • docker_host — installed Docker CE and Docker Compose
  • drone — deployed Drone Server as a Docker Compose service
  • drone_runner — deployed Drone Runner, connecting it to Drone Server
  • hugo_uat — deployed an nginx container to serve Hugo static files

The Ansible playbook also bootstrapped CA trust on the host — a critical step covered in detail in the next section.

# Simplified: roles applied to <pipeline-host>
- hosts: pipeline_host
  roles:
    - docker_host
    - smallstep_bootstrap   # install root CA into host trust store
    - drone
    - drone_runner
    - hugo_uat

The roles are modular and reusable. If <pipeline-host> is destroyed, running tofu apply followed by ansible-playbook site.yml recreates it from scratch — including the certificate trust configuration.


The Certificate Problem: Why Drone Fails Without Proper CA Trust

This is the most technically interesting part of the setup. Getting it right requires understanding how Docker container trust stores work, and where in the startup sequence CA trust must be established.

The Core Problem Chain

Drone Server runs as a Docker container. Its first job when a pipeline triggers is to connect back to Gitea over HTTPS to fetch repository details and update commit status. Gitea is served behind Caddy, which terminates TLS using a certificate issued by Smallstep step-ca.

Docker containers do not automatically inherit the host’s CA trust store. A container started from a standard image (like drone/drone:2) ships with the CA bundle baked into the image — that bundle contains public CAs like DigiCert and Let’s Encrypt, but not your homelab’s Smallstep root certificate.

The result is a x509: certificate signed by unknown authority error every time Drone tries to reach Gitea.

The Fix: Volume-Mounting the Host CA Bundle

The Ansible role for Drone mounts the host’s /etc/ssl/certs directory into the container:

# docker-compose.yml for Drone Server (simplified)
services:
  drone:
    image: drone/drone:2
    volumes:
      - /etc/ssl/certs:/etc/ssl/certs:ro
    environment:
      - DRONE_GITEA_SERVER=https://git.<your-domain>
      - DRONE_SERVER_HOST=ci.<your-domain>
      - DRONE_SERVER_PROTO=https
      # ...

The :ro mount gives the container a read-only view of the host’s CA certificates, including any custom CAs added to the host. As long as the Smallstep root CA is in the host’s /etc/ssl/certs before the container starts, Drone trusts it.

This works — but only if the host trust store is correctly populated. That requires solving the bootstrap problem first.

The Bootstrap Chicken-and-Egg Problem

To install the Smallstep root CA into the host trust store, you run step ca bootstrap:

step ca bootstrap \
  --ca-url https://ca.<your-domain> \
  --fingerprint <root-ca-fingerprint>

This command downloads the root CA certificate from the step-ca server and installs it. The problem is that step ca bootstrap connects to the step-ca server over HTTPS, and that server’s TLS certificate is signed by… the Smallstep root CA you haven’t trusted yet.

step ca bootstrap --ca-url https://ca.<your-domain> --fingerprint <fp>
# Error: x509: certificate signed by unknown authority

The solution is the --insecure flag:

step ca bootstrap \
  --ca-url https://ca.<your-domain> \
  --fingerprint <fp> \
  --insecure

This skips verification of the step-ca server’s TLS certificate for the bootstrap request only. After the root CA is installed, all subsequent connections are properly verified. You are trusting the CA fingerprint, not the TLS certificate — the fingerprint is a hash of the root CA public key, which you supply out-of-band.

The Ansible role handles this via the smallstep_ca_insecure: true variable:

# Ansible task: Bootstrap Smallstep CA trust on host
- name: Bootstrap Smallstep CA
  shell: |
    step ca bootstrap \
      --ca-url {{ smallstep_ca_url }} \
      --fingerprint {{ smallstep_ca_fingerprint }} \
      {% if smallstep_ca_insecure %}--insecure{% endif %} \
      --install
  args:
    creates: /usr/local/share/ca-certificates/smallstep-root.crt

- name: Update CA certificate bundle
  command: update-ca-certificates

After update-ca-certificates runs, the Smallstep root is in /etc/ssl/certs/ca-certificates.crt. The Drone container mounts this directory and inherits full trust.

The Full Certificate Trust Chain

Once bootstrap completes, the trust chain from browser to Gitea looks like this:

Smallstep Root CA (self-signed, installed on <pipeline-host> host)
  └─ Intermediate CA (step-ca internal)
       └─ git.<your-domain> TLS cert
            (issued to Caddy via ACME, renewed automatically)

When Drone Server makes an HTTPS request to git.<your-domain>:

  1. Caddy presents the git.<your-domain> TLS certificate
  2. Drone’s Go TLS stack traverses the certificate chain
  3. It finds the Smallstep Root CA in the mounted /etc/ssl/certs
  4. Trust verified — connection proceeds
Trust Setup (one-time)
🔐
step-ca
Root CA
🖥
<pipeline-host> OS
/etc/ssl/certs (trust store)
step ca bootstrap --insecure (one-time)
Runtime Flows
🐳
Drone Server Container
Trusts internal CA via :ro mount
📦
git.<your-domain>
Gitea HTTPS ✓ cert trusted
HTTPS (cert trusted)
🌐
ci.<your-domain> via Caddy
DRONE_SERVER_PROTO=https required
OAuth callback (HTTPS)
Docker volume mount :roBuild Execution
🔄
Drone Runner
Spawns build containers
🐳
Build Container
Ephemeral — no internal CA trust
spawns
🌐
nginx UAT
rsync over SSH ✓ no TLS cert needed
rsync / SSH
☁️
Firebase Hosting
firebase deploy ✓ public CA only
firebase deploy
Text is not SVG - cannot display
Certificate trust chain in the Drone CI/CD setup. Vermillion arrows show certificate flows; dashed vermillion shows the volume mount. Sumi ink arrows show request flows. Note that ephemeral build containers do not inherit CA trust.

The DRONE_SERVER_PROTO Issue

One configuration detail catches people out. Drone Server runs HTTP internally on port 3000. Caddy terminates TLS externally on port 443 and proxies to port 3000. Drone must be told its external address:

environment:
  - DRONE_SERVER_HOST=ci.<your-domain>
  - DRONE_SERVER_PROTO=https

If DRONE_SERVER_PROTO is omitted or set to http, Drone generates OAuth redirect URLs with http://ci.<your-domain>/.... Gitea redirects the browser to that URL after OAuth authorisation, the browser follows it, and the redirect never matches the expected HTTPS callback. The result is an OAuth loop that is frustrating to debug.

Setting DRONE_SERVER_PROTO=https tells Drone to generate https://ci.<your-domain>/... callbacks, which Caddy can route correctly.

The Drone Runner and Ephemeral Container Problem

Drone Runner executes pipeline steps in ephemeral Docker containers — for example, hugomods/hugo for the build step, or alpine for the deploy step. These containers are started fresh for every build. They do not inherit the host’s CA bundle, and they do not inherit it from the Drone Runner container.

If a pipeline step needs to reach an internal HTTPS endpoint, it will fail with the same x509: certificate signed by unknown authority error.

For this Hugo deployment, the problem is avoided by design:

  • The build step only runs hugo — no network calls to internal services
  • The UAT deploy step uses rsync over SSH, not HTTPS, to copy files to <pipeline-host>
  • The Firebase deploy step uses the Firebase CLI, which connects only to Google’s infrastructure (public CA, no internal cert needed)

SSH does not use TLS certificate verification in the same way — it uses host key fingerprints, which the pipeline manages via ssh-keyscan. Certificate trust is irrelevant for the deployment step.

If a future pipeline step did need to reach an internal HTTPS service, the fix would be to mount or copy the root CA into the container at step start, or to provide a custom base image with the CA pre-installed.

Common Failure Modes

For reference, here are the failure modes encountered in this setup and their causes:

Error Cause Fix
x509: certificate signed by unknown authority Drone container does not trust Smallstep root Mount /etc/ssl/certs:ro from host; ensure bootstrap ran first
x509: certificate signed by unknown authority during bootstrap Chicken-and-egg: step-ca TLS not yet trusted Use step ca bootstrap --insecure
OAuth redirect loop in Drone DRONE_SERVER_PROTO=http while Caddy serves HTTPS Set DRONE_SERVER_PROTO=https
Pipeline step fails on internal HTTPS call Ephemeral container has no internal CA trust Use SSH instead of HTTPS for deployment; or inject CA into step
Caddy fails to issue certificate step-ca unreachable or ACME provisioner misconfigured Check step-ca logs; verify DNS resolution for ca.<your-domain>

Smallstep step-ca: ACME Configuration for Caddy

With the trust problem solved, let’s look at how Caddy actually gets its certificates.

ACME Provisioner Configuration

step-ca’s config/ca.json defines an ACME provisioner that Caddy authenticates against:

{
  "provisioners": [
    {
      "type": "ACME",
      "name": "homelab",
      "claims": {
        "maxTLSCertDuration": "24h",
        "minTLSCertDuration": "5m",
        "defaultTLSCertDuration": "24h"
      }
    }
  ]
}

Certificates are valid for 24 hours. This is intentional — short-lived certificates limit exposure in the event of key compromise. Caddy handles renewal automatically, so there is no operational overhead.

Caddy Configuration for Internal ACME

The Caddyfile tells Caddy to use step-ca as its ACME provider:

uat.<your-domain> {
    reverse_proxy <pipeline-host>:8080

    tls {
        issuer acme {
            dir https://ca.<your-domain>:9000/acme/homelab/directory
            trusted_roots /etc/caddy/step-ca-root.pem
        }
    }
}

ci.<your-domain> {
    reverse_proxy <pipeline-host>:3000
    tls {
        issuer acme {
            dir https://ca.<your-domain>:9000/acme/homelab/directory
            trusted_roots /etc/caddy/step-ca-root.pem
        }
    }
}

trusted_roots points Caddy to the Smallstep root CA certificate — Caddy needs this to verify the step-ca ACME endpoint itself when requesting certificates. This is the same trust problem in a different location: Caddy needs to trust step-ca before it can request a cert from step-ca.


The Hugo CI/CD Pipeline

With certificate trust established, the Drone pipeline for Hugo is straightforward.

Pipeline Definition

# .drone.yml
kind: pipeline
type: docker
name: hugo

trigger:
  branch:
    - develop
    - main

steps:
  - name: build
    image: hugomods/hugo:exts
    commands:
      - hugo --destination ./public --minify
    when:
      branch: [develop, main]

  - name: deploy-uat
    image: alpine:latest
    commands:
      - apk add --no-cache rsync openssh-client
      - mkdir -p ~/.ssh
      - echo "$${DEPLOY_SSH_KEY}" > ~/.ssh/id_ed25519
      - chmod 600 ~/.ssh/id_ed25519
      - ssh-keyscan -H <pipeline-host> >> ~/.ssh/known_hosts
      - rsync -avz --delete ./public/ deploy@<pipeline-host>:/opt/hugo-uat/html/
    secrets:
      - deploy_ssh_key
    when:
      branch: develop

  - name: deploy-firebase
    image: node:20-alpine
    commands:
      - npm install -g firebase-tools
      - firebase deploy --token "$${FIREBASE_TOKEN}" --project ebkac-hugo-webpage --only hosting
    secrets:
      - firebase_token
    when:
      branch: main

The pipeline has three steps:

  1. build — Hugo generates static HTML/CSS/JS to ./public/. This step runs on both branches.
  2. deploy-uat — rsync copies ./public/ to /opt/hugo-uat/html/ on <pipeline-host> via SSH. The nginx container serves from that directory. Caddy proxies https://uat.<your-domain> to port 8080 on <pipeline-host>.
  3. deploy-firebase — Firebase CLI uploads the built site to Firebase Hosting. Only runs on main.

Drone Secrets

Two secrets are stored in Drone’s secret store (set via the Drone UI):

  • deploy_ssh_key — the private SSH key that authorises the rsync user on <pipeline-host>. The corresponding public key is in ~deploy/.ssh/authorized_keys on the host, managed by Ansible.
  • firebase_token — obtained from firebase login:ci and stored in Drone. Used only in the deploy-firebase step.

Neither secret is committed to Git.

Why rsync over SSH and Not HTTPS

The deployment step deliberately avoids HTTPS. rsync over SSH requires only that the host’s SSH key fingerprint is known (via ssh-keyscan) — it does not depend on TLS certificate trust. This sidesteps the ephemeral container CA problem entirely.

An alternative would be to expose the nginx container over HTTPS via Caddy and deploy via HTTPS. That would require injecting the Smallstep root CA into every build container. SSH is simpler here.


Lessons and Operational Notes

Order Matters During Initial Provisioning

The Ansible role order is important. smallstep_bootstrap must run before drone, because the bootstrap installs the root CA into the host trust store. If Drone starts before the CA is trusted, the container mounts an incomplete trust store and Gitea connections fail.

If you run Ansible in parts or if roles run in the wrong order, restart the Drone container after bootstrap completes:

docker compose -f /opt/drone/docker-compose.yml restart drone

Short-Lived Certificates Require Reliable step-ca

Certificates valid for 24 hours mean Caddy renews them daily. If step-ca is unavailable at renewal time, the existing certificate continues to serve until expiry — at which point Caddy has nothing valid to offer. Monitor step-ca availability and certificate expiry.

A useful check:

# Check current certificate expiry for a site
echo | openssl s_client -connect uat.<your-domain>:443 2>/dev/null \
  | openssl x509 -noout -enddate

Disaster Recovery for the Root CA

If the step-ca private key is lost, all certificates become unrenewable. When they expire, every internal HTTPS service goes dark simultaneously. Back up the step-ca data directory to a separate host and test restoration.

Debugging Certificate Problems

# Check Drone container logs for TLS errors
docker logs drone 2>&1 | grep -i "x509\|tls\|certificate"

# Verify the host trust store includes the Smallstep root
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \
  <(echo | openssl s_client -connect git.<your-domain>:443 2>/dev/null \
    | openssl x509)

# Manually test the ACME endpoint from the Caddy host
curl -v --cacert /etc/caddy/step-ca-root.pem \
  https://ca.<your-domain>:9000/acme/homelab/directory

Conclusion

The most important lesson from this setup is that running HTTPS services in Docker requires explicit, deliberate CA trust management. The host OS and Docker containers have separate trust stores, and making containers trust an internal CA means both solving the bootstrap problem and ensuring the trust propagates at container start time.

Mounting /etc/ssl/certs:ro from the host into the Drone container is a pragmatic solution. It keeps the container image generic — no custom builds needed — while giving it access to whatever CAs the host trusts. The bootstrap step, with its --insecure flag, is necessary and safe: you verify the CA by fingerprint, not by TLS chain.

The IaC layer (OpenTofu + Ansible) makes this reproducible. If <pipeline-host> disappears, three commands recreate it — including the bootstrap trust, the Drone configuration, and the nginx UAT server. The pre-existing services (step-ca, Caddy, Gitea) remain untouched.

The Hugo pipeline itself is simple. Build with Hugo, deploy to UAT via rsync over SSH, deploy to Firebase for production. The complexity was always in the certificate plumbing underneath.


Summary

  • Docker containers do not inherit host CA trust. A container started from a standard image trusts only the CAs baked into that image. Internal CA certificates must be explicitly provided.
  • Volume-mounting /etc/ssl/certs:ro from the host is the recommended pattern for giving containers access to the host trust store. It requires the host trust store to be correctly populated before the container starts.
  • The bootstrap problem is real. Installing a root CA cert via step ca bootstrap requires connecting to the step-ca server, which presents a cert signed by that same root CA. The --insecure flag breaks the circular dependency; the fingerprint provides out-of-band verification.
  • Drone’s DRONE_SERVER_PROTO=https is required when Caddy terminates TLS in front of Drone. Without it, OAuth callbacks use http:// and break authentication.
  • Ephemeral build containers have no CA trust. The pipeline sidesteps this by using rsync over SSH (not HTTPS) for UAT deployment, and Firebase (public CA) for production.
  • IaC provisioned the Docker host and pipeline services. step-ca, Caddy, Gitea, and DNS were pre-existing prerequisites, not outputs of the IaC.
  • Short-lived certificates require a reliable step-ca. Monitor availability and certificate expiry. Back up the root CA key.

References

Core Components

  • Hugo — Static Site Generator — Official documentation for Hugo, including configuration, templates, and content management.
  • Smallstep step-ca — Documentation for the open-source certificate authority used throughout this article.
  • Smallstep ACME Provisioner — How to configure the ACME provisioner in step-ca for automated certificate issuance.
  • step ca bootstrap — Reference for the bootstrap command, including the --insecure flag.
  • Caddy — Automatic HTTPS — Caddy’s documentation on how it handles TLS certificates automatically via ACME.
  • Caddyfile Reference — Full reference for the Caddyfile configuration format.
  • Drone CI/CD — Official documentation for Drone, including pipeline configuration and runner setup.
  • Gitea — Self-hosted Git service documentation.

Infrastructure as Code

  • OpenTofu — The open-source Terraform fork used for infrastructure provisioning.
  • Ansible Documentation — Automation platform for configuration management and application deployment.
  • Proxmox VE — Proxmox Virtual Environment documentation covering VM and LXC container management.

Standards and Protocols

Hosting and Deployment

  • Firebase Hosting — Google’s static hosting platform used for production deployment in this project.

Related: The Firebase Hosting production deployment described here is the same site discussed in EBKAC.ORG Goes Live: Custom Domains, Firebase, and the Grayed-Out Button, which covers domain registration, Firebase REST API custom domain setup, and DNS configuration.