TutorialsJan 6, 202614 min read

Multi-environment DNS automation on Cloudflare using CircleCI and Terraform

Olususi Oluyemi

Fullstack Developer and Tech Author

Manually configuring DNS records for staging and production environments is a common pain point for developers and DevOps teams. As your organization grows and you manage more applications across different services, keeping DNS records up-to-date and error-free becomes increasingly challenging and time-consuming. Mistakes in DNS setup can lead to downtime, broken environments, or confusing deployments, especially when juggling multiple teams or microservices.

This tutorial will show you how to eliminate those manual headaches by fully automating DNS management for your applications using a modern stack: Terraform for Infrastructure as Code, CircleCI for continuous integration and deployment, Cloudflare for DNS hosting, and Fly.io for global app deployment.

You’ll build a minimal Go application, deploy it to both staging and production environments on Fly.io, and use Terraform to manage DNS records for each environment. CircleCI will orchestrate the process, automatically updating DNS records whenever you push changes to your repository.

By the end of this guide, you’ll have a robust, repeatable pipeline that keeps your DNS records in sync with your deployments, giving you reliability, confidence, and more time to focus on building great software.

Prerequisites

The following are required to follow along with this tutorial:

  • Go (1.24 or later) installed on your local machine (macOS or Unix-based system recommended).
  • A CircleCI account.
  • A GitHub account.
  • A Fly.io account. Fly.io offers a free tier for global app deployment, but may require a payment method for TLS certificates or custom domains.
  • The Fly.io CLI installed locally.
  • Cloudflare account. A free account is sufficient.
  • Docker installed locally.
  • Terraform installed locally.
  • Basic knowledge of Go programming language.
  • A domain name to set up DNS records. You can purchase one from any registrar (e.g., Namecheap, GoDaddy), or use an existing domain you own. Make sure your domain is pointed to Cloudflare for DNS automation. We will cover how to set this up later in the tutorial.

Initialize your project

To get started with multi-environment DNS automation, you’ll first set up a minimal Go application and organize your project structure to support both staging and production deployments. This foundation will make it easy to automate DNS management and CI/CD workflows later in the tutorial.

Create and initialize the Go project

Begin by creating a new directory for your project and initializing a Go module. In your terminal, run these commands:

mkdir multi-env-dns-cloudflare
cd multi-env-dns-cloudflare
go mod init github.com/CIRCLECI-GWP/multi-env-dns-cloudflare

Note: The module path github.com/yemiwebby/multi-env-dns-cloudflare matches the author’s GitHub repo. If you use your own GitHub username, update the go.mod file and any internal import paths to match.

This sequence creates a new folder, opens it, and initializes your Go module.

Organize your project structure

Keep things tidy by creating separate folders for your Terraform files—one for staging and one for production. Run:

mkdir -p terraform/staging terraform/production

This creates a terraform folder with dedicated subfolders for staging and production. You’ll use these directories to store environment-specific Terraform files as you progress through the tutorial.

Building the Go application

Now, create your main application file. This simple Go server responds with a message indicating which environment it’s running in, based on the APP_ENV environment variable.

Create a file named main.go in the root of your project directory. Add this code:

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	env := os.Getenv("APP_ENV")
	if env == "" {
		env = "unknown"
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Welcome to the %s environment!", env)
	})

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	log.Printf("Starting server in %s on port %s", env, port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

This Go app reads the APP_ENV variable to determine its environment (staging or production), sets up a simple HTTP server, and prints a message to the browser or API client. The port can be customized via the PORT environment variable.

Containerize your Go application

To deploy your Go app to Fly.io, you’ll first need to package it as a Docker container. This makes your app portable and ensures it runs consistently across environments.

Create a Dockerfile

In the root of your project, create a file named Dockerfile. Add this content:

FROM golang:1.24-alpine

WORKDIR /app

COPY . .

RUN go build -o server .

EXPOSE 8080

CMD ["./server"]

This Dockerfile uses a lightweight Go image, copies your code into the container, builds the Go binary, exposes port 8080, and sets the default command to run your server.

Deploying to Fly.io

With your Dockerfile in place, you can now deploy your app to Fly.io; a platform for running apps globally with minimal setup.

Authenticate with Fly.io

First, use the CLI to log into Fly.io:

fly auth login

This command will open a browser window for you to authenticate with your Fly.io account. If you don’t have an account, you can create one during this process.

Initialize your Fly.io app

Next, without deploying it, initialize your application:

fly launch --name go-api-staging --no-deploy

This command sets up a new Fly.io app called go-api-staging and generates a fly.toml configuration file in your project directory. You can review and customize this file as needed.

Your fly.toml should contain this content:

# fly.toml app configuration file generated for go-api-staging on 2025-07-11T21:12:33+01:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = 'go-api-staging'
primary_region = 'lhr'

[build]

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

[[vm]]
  memory = '1gb'
  cpu_kind = 'shared'
  cpus = 1

The fly.toml file defines the name, region, build settings, HTTP service configuration, and VM resources of your app. You can adjust these settings to fit your needs or leave them as defaults, which should work for most use cases.

Set secrets and deploy your app

Before deploying, set the environment variable for your staging app on Fly.io. This tells your Go application which environment it’s running in.

fly secrets set APP_ENV=staging --app go-api-staging
fly deploy --app go-api-staging

Repeat the process for production. First, create the production app:

fly apps create go-api-prod

Then set the environment variable and deploy:

fly secrets set APP_ENV=production --app go-api-prod
fly deploy --app go-api-prod

Once deployed, you’ll have two URLs:

You can test both endpoints with:

curl https://go-api-staging.fly.dev
curl https://go-api-prod.fly.dev

Your output:

Welcome to the staging environment!
Welcome to the production environment!

Terraform DNS configuration

Now you can automate DNS setup for both environments using Terraform and Cloudflare.

Staging DNS record

To automate DNS for your staging environment, you’ll use Terraform to create a CNAME record in Cloudflare. This record will point your custom subdomain (like api.staging.demoapi.biz) to your Fly.io staging app.

In terraform/staging/main.tf, add:

terraform {
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
  }
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

resource "cloudflare_record" "staging_api" {
  zone_id = var.cloudflare_zone_id
  name    = "api.staging"
  type    = "CNAME"
  content   = "go-api-staging.fly.dev"
  ttl     = 120
  proxied = false
  allow_overwrite = true
}

This configuration tells Terraform to use the Cloudflare provider, authenticate with your API token, and create a DNS record named api.staging in your domain’s zone. The record is a CNAME pointing to your staging Fly.io app, with a Time to live of 120 seconds. Setting proxied to false means Cloudflare will not proxy traffic. allow_overwrite lets Terraform update the record if it already exists.

Production DNS record

You’ll follow the same process for your production environment, but the DNS record will point to your production Fly.io app and use a different subdomain.

In terraform/production/main.tf, add:

terraform {
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
  }
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

resource "cloudflare_record" "prod_api" {
  zone_id = var.cloudflare_zone_id
  name    = "api"
  type    = "CNAME"
  content   = "go-api-prod.fly.dev"
  ttl     = 120
  proxied = false
  allow_overwrite = true
}

This creates a CNAME record named api (for api.demoapi.biz) that points to your production Fly.io app. The rest of the configuration is similar to staging, ensuring your production endpoint is always up-to-date and managed automatically.

With these Terraform files in place, you can easily manage DNS records for both environments, keeping your endpoints in sync with your deployments and reducing manual errors.

Terraform variables

Create a variables.tf file in both terraform/staging and terraform/production directories and use the following content:

variable "cloudflare_api_token" {
  description = "API token for Cloudflare"
  type        = string
}

variable "cloudflare_zone_id" {
  description = "Zone ID of the Cloudflare-managed domain"
  type        = string
}

These variables allow you to securely pass your Cloudflare API token and zone ID to Terraform, so it can manage DNS records for your domain.

Connect your domain to Cloudflare

To automate DNS management with Terraform and Cloudflare, your domain must be connected to Cloudflare. This gives you full control over DNS records and lets Terraform update them automatically.

Add your domain to Cloudflare

  1. Go to Cloudflare Dashboard.
  2. Click Add a domain.
  3. Enter your domain name (e.g., demoapi.biz).
  4. Select the Free Plan.
  5. Cloudflare will scan your existing DNS records. You can skip or fix these later.
  6. Click Continue to activation.

If prompted about DNS records, you can skip this step for now. You’ll manage DNS records with Terraform later.

Get Cloudflare nameservers

After setup, Cloudflare will provide two nameservers, similar to:

chuck.ns.cloudflare.com
dawn.ns.cloudflare.com

Copy these nameservers. You will need them to update your domain registrar.

Update nameservers in domain registrar

  1. Log into your domain registrar (e.g., Namecheap).
  2. Go to your Domain List.
  3. Click Manage next to your domain.
  4. Under Nameservers, choose Custom DNS.
  5. Paste the two Cloudflare nameservers.
  6. Save changes.

DNS propagation is usually fast (often under 10 minutes), but it may show as “Pending” for a short while.

Please note that the steps to update nameservers may vary slightly depending on your domain registrar. If you’re using a different registrar, refer to their documentation for updating nameservers.

Wait for Cloudflare verification

Once Cloudflare detects the correct nameservers:

  • Your domain will be marked as active.
  • You’ll unlock the full dashboard and DNS settings.

Cloudflare domain active

Create a Cloudflare API token for Terraform DNS automation

To let CircleCI and Terraform manage your DNS records on Cloudflare, you’ll need an API token with the right permissions. Follow these steps to create one:

  1. Go to Cloudflare API Tokens.
  2. Click Create Token.
  3. Choose the “Edit zone DNS” template.
    This template is already set up with the permissions needed for Terraform to manage DNS records.
  4. Under Zone Resources, select:
    • Include → Specific zone
    • Pick your domain (e.g., demoapi.biz)
  5. Continue to Summary and then click Create Token.
  6. Copy and save the token somewhere safe—you’ll need it for CircleCI and Terraform.

Note: You won’t be able to view the token again after closing the dialog.

Finding your Cloudflare Zone ID for your active domain

You’ll also need your Zone ID for CircleCI and Terraform:

  1. Go to the Cloudflare Dashboard.
  2. Select your domain.
  3. In the Overview tab, copy the value in the Zone ID field to use later.

TLS certificates setup on Fly.io

Fly.io requires a valid TLS certificate to be in place before you can deploy your app with a custom domain. This ensures your app is served over HTTPS right from the start, providing security and trust for your users.

You should run Terraform locally to provision your DNS records before adding certificates. This guarantees your custom domains are set up and ready for Fly.io to issue certificates.

From this point onward, be sure to replace api.staging.demoapi.biz and api.demoapi.biz with your actual registered domain names.

Set environment variables locally

Export your Cloudflare API token and zone ID so Terraform can use them:

export TF_VAR_cloudflare_api_token=your_cloudflare_api_token
export TF_VAR_cloudflare_zone_id=your_cloudflare_zone_id

Initialize and apply Terraform for staging:

cd terraform/staging
terraform init
terraform apply

Then do the same for production:

cd ../production
terraform init
terraform apply

Set up staging and production certificates

Add SSL certificates for both environments:

fly certs add api.staging.demoapi.biz --app go-api-staging
fly certs add api.demoapi.biz --app go-api-prod

Verify certificates

Wait a few minutes for the certificates to be issued. Check their status with:

fly certs check api.staging.demoapi.biz --app go-api-staging
fly certs check api.demoapi.biz --app go-api-prod

While Fly.io offers a free tier, adding custom domains with TLS certificates may prompt you to add a payment method, even if your app usage is within the free allowance.

If your certificates stop working or your app becomes unreachable, it’s likely because your trial has ended or payment details are missing.

For a purely free experience with custom domains and HTTPS, consider platforms like Render or Railway, though their deployment and DNS workflows differ.

You can also check the status of your app:

fly status --app go-api-staging

You may get this output:

Error: failed to list active VMs: trial has ended, please add a credit card by visiting https://fly.io/trial (Request ID: 11111111-lhr)

If you do, it means you need to add a payment method to your Fly.io account to continue using custom domains with TLS certificates.

Create CircleCI cnfiguration

To automate your DNS updates whenever you push code, you’ll use CircleCI to run Terraform for both staging and production environments. The configuration below sets up two jobs, one for staging and one for production, using parameterized configurations. The jobs are triggered by changes to the develop and main branches, respectively.

Create a .circleci/config.yml file in your project. Add this content:

version: 2.1

executors:
  terraform-executor:
    docker:
      - image: hashicorp/terraform:1.6
    working_directory: ~/project

jobs:
  deploy-dns:
    parameters:
      env:
        type: string
    executor: terraform-executor
    steps:
      - checkout
      - run:
          name: Terraform Init.
          command: |
            cd terraform/<< parameters.env >>
            terraform init
      - run:
          name: Terraform Apply
          command: |
            cd terraform/<< parameters.env >>
            terraform apply -auto-approve

workflows:
  deploy:
    jobs:
      - deploy-dns:
          name: staging-dns
          env: "staging"
          filters:
            branches:
              only: develop

      - deploy-dns:
          name: production-dns
          env: "production"
          filters:
            branches:
              only: main

This configuration uses a Docker image with Terraform installed, checks out your code, and runs terraform init and terraform apply in the appropriate environment directory. The staging-dns job runs when you push to develop, and the production-dns job runs when you push to main.

Once you’ve added this file, save your changes and push your code to GitHub.

Setting up the project in CircleCI

Log into CircleCI and select your multi-env-dns-cloudflare repository from the project list.

CircleCI project setup

Choose the develop branch to trigger the CircleCI pipeline for staging and click Set Up Project. This will apply the Terraform configuration for the staging environment and trigger your first build automatically. The build will fail on the first run, because Terraform requires the Cloudflare API token and zone ID to be set as environment variables.

CircleCI pipeline failure

To fix this, you need to set the required environment variables in CircleCI. These variables will allow Terraform to authenticate with Cloudflare and manage your DNS records.

Create environment variables in CircleCI

In CircleCI, go to your project settings and add the following environment variables:

  • CLOUDFLARE_API_TOKEN: Your Cloudflare API token.
  • CLOUDFLARE_ZONE_ID: Your Cloudflare zone ID for the domain you set up earlier.

Now that you have set the environment variables, you can rerun the pipeline.

This should trigger the staging-dns job, creating the DNS record for api.staging.demoapi.biz.

CircleCI pipeline develop success

Deploying to production

Now merge the develop branch into main to trigger the production deployment. This will apply the Terraform configuration for the production environment.

git checkout main
git merge develop
git push origin main

When you merge to main, it will trigger the production-dns job, creating the DNS record for api.demoapi.biz.

CircleCI pipeline main success

How to test

To make sure everything is working, test your staging and production endpoints using curl or your browser:

curl https://api.staging.demoapi.biz
curl https://api.demoapi.biz

You should receive the expected environment message from your Go app.

Understanding the building blocks

  • Fly.io: hosts your Go app and provides a public domain (like go-api-staging.fly.dev).
  • Cloudflare: manages DNS for your purchased domain (demoapi.biz).
  • Terraform: automates DNS record creation in Cloudflare, pointing your custom domains to Fly.io.
  • CircleCI: runs Terraform when you push to develop or main, wiring up DNS automatically.

Understanding the flow

Here’s how it all comes together for staging, for example. You deploy your Go app to Fly.io, which makes it publicly accessible at https://go-api-staging.fly.dev.

Next, you connect your custom domain (demoapi.biz) to Cloudflare, giving you control over DNS entries like api.staging.demoapi.biz and api.demoapi.biz.

With Terraform, you automate the creation of DNS records so that any requests to api.staging.demoapi.biz are routed directly to your Fly.io app. HTTPS certificates are provisioned with Fly.io, ensuring your custom domains are secure from the start.

Finally, CircleCI ties everything together by running this process automatically whenever you push changes to your Git branches, deploying to the staging domain from develop and to the production domain from main.

Cleaning up resources

If you want to remove all the resources you created during this tutorial, you’ll need to clean up both your DNS records in Cloudflare and your applications on Fly.io.

Remove DNS records with Terraform

Navigate to each environment’s Terraform directory and run the destroy command. This will delete the DNS records you created in Cloudflare.

For staging:

cd terraform/staging
terraform destroy

For production:

cd ../production
terraform destroy

Terraform will prompt you to confirm before deleting the resources.

Delete your apps from Fly.io

To remove your applications from Fly.io, use the CLI:

fly apps destroy go-api-staging
fly apps destroy go-api-prod

You’ll be asked to confirm each deletion.

Cleaning up these resources ensures you won’t incur any unexpected charges and keeps your Cloudflare and Fly.io accounts tidy.

Conclusion

By combining Terraform, Cloudflare, Fly.io, and CircleCI, you’ve automated DNS setup for multiple environments in a clean and scalable way. This reduces manual errors, keeps environments consistent, and streamlines your deployment workflow.

Whether you’re deploying microservices, staging previews, or production APIs, this strategy ensures each environment gets a reliable endpoint with proper HTTPS, no manual steps required.

With your DNS automation in place, you’re ready for even more advanced workflows.

Happy shipping!

If you’d like to explore the complete source code or use it as a reference for your own projects, you can find everything on GitHub.


Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest in solving day-to-day problems encountered by users, he ventured into programming and has since directed his problem solving skills at building software for both web and mobile. A full stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and blog posts on several blogs around the world. Being tech savvy, his hobbies include trying out new programming languages and frameworks.