Create Docker registries using Terraform
Provision a Harness Artifact Registry virtual registry, an upstream proxy to Docker Hub, and link them as infrastructure as code with the Harness Terraform provider.
This tutorial walks you through creating a fully functional Docker registry in Harness Artifact Registry using Terraform, an open-source infrastructure-as-code tool that lets you define cloud resources in configuration files instead of clicking through a UI.
Instead of creating registries by hand in the Harness console, you write a few short configuration files and run a single command. The result is the same (a working Docker registry), but the setup is now version-controlled, repeatable, and auditable.
What you will build
A virtual registry, the URL your team uses to pull and push Docker images.
An upstream proxy, a caching layer that connects to Docker Hub, so the first time someone pulls a public image (like
nginxoralpine), it is fetched from Docker Hub and cached in Harness. Every pull after that is served from the cache.A link between them, so pulls from your virtual registry automatically resolve through Docker Hub when needed.
Your team runs:
docker pull <your-registry>/library/nginx:latest
│
▼
┌─────────────────────────┐
│ my-docker (VIRTUAL) │ ← Your team points Docker here
│ │
│ Checks local cache │
│ Cache miss? Ask proxy: │
│ │ │
│ ▼ │
│ dockerhub-proxy │ ← Fetches from Docker Hub
│ (UPSTREAM) │ and caches the result
└─────────────────────────┘Before you begin
Make sure you have the following:
A Harness account with Artifact Registry enabled. Sign up if you do not have one.
Terraform v1.5.0 or later installed on your machine. Go to Install Terraform to download it.
Docker installed on your machine (used to verify the registry at the end). Go to Install Docker to set it up.
A Harness API key: a Personal Access Token (PAT) or Service Account Token. Generate one in the Harness UI under My Profile > + API Key > + Token. This is how Terraform authenticates with Harness.
Your project identifiers, three values:
Account ID: visible in your Harness URL or under Account Settings > Overview.
Organization ID: the identifier of your Harness organization (for example,
default).Project ID: the identifier of the project where you want to create the registries.
Overview
This tutorial has three steps. Each step creates its own Terraform configuration in a separate directory:
Step 1
Creates an empty virtual registry (the client-facing endpoint).
01-virtual-registry/
Step 2
Creates an upstream proxy connected to Docker Hub.
02-upstream-proxy/
Step 3
Links the proxy to the virtual registry so pulls resolve through Docker Hub.
03-link-virtual/
Why three separate steps? Each step manages a different resource with its own Terraform state. You can update the upstream proxy (for example, switching from anonymous to authenticated Docker Hub access) without touching the virtual registry, or attach more upstream proxies to the same virtual registry later.
Each directory contains these files:
versions.tf
Declares the Terraform version and the Harness provider dependency.
providers.tf
Configures how Terraform authenticates with Harness.
variables.tf
Defines the input parameters (account ID, API key, registry name, and so on).
main.tf
The core resource definition, where the registry is actually created.
outputs.tf
Values exported after creation (registry URL, identifier, and so on) for use in later steps.
You do not need to memorize this. Each file is shown in full below.
Step 1: Create the virtual registry
The virtual registry is the entry point for your team's Docker operations. In this first step, you create it without any upstream proxy attached. The proxy connection is made in Step 3.
Create a directory called 01-virtual-registry and add the following five files.
versions.tf declares which provider to download. The Harness provider is a plugin that knows how to talk to the Harness API.
providers.tf configures the Harness provider with your credentials. The values come from the variables in the next file.
variables.tf defines the inputs Terraform needs. You provide the values when you run terraform apply.
main.tf is the core of Step 1; it defines the virtual registry resource.
What each attribute does:
identifier
A unique name for this registry within your project. Becomes part of the URL you use with docker pull.
space_ref
Tells Harness which project to create the registry in. Format: account_id/org_id/project_id.
package_type
The type of artifacts this registry holds. DOCKER for Docker images. Other options include HELM, MAVEN, NPM, PYPI, GO, NUGET, and CARGO.
config.type
VIRTUAL means this is a client-facing registry (as opposed to UPSTREAM, which is a proxy).
config.upstream_proxies
A list of upstream proxy identifiers to route through. Empty for now; populated in Step 3.
outputs.tf prints values after Terraform finishes; later steps can reference them.
The import_command_for_step3 output generates a command you need in Step 3. Terraform prints it for you; save it for later.
Now run Terraform. First, set your credentials as environment variables in your terminal.
Replace the placeholder values with your actual account ID, API key, and project scope.
PROTECT YOUR API KEY
Never commit API keys to version control. Add a .gitignore file to your project root with:
Then initialize and apply:
terraform initdownloads the Harness provider plugin.terraform validatechecks your configuration for syntax errors.terraform applyshows you a plan of what will be created and asks for confirmation.
Type yes when prompted. You should see:
Save the import_command_for_step3 value. You need it in Step 3.
At this point, the virtual registry exists in Harness but cannot resolve external images yet because it has no upstream proxy. That comes next.
Step 2: Create the upstream proxy
The upstream proxy connects to Docker Hub and caches images locally in Harness. This is an independent resource that does not depend on the virtual registry you created in Step 1.
Create a directory called 02-upstream-proxy and add the following five files.
versions.tf is the same as Step 1:
providers.tf is the same as Step 1:
variables.tf adds variables for Docker Hub authentication. By default the proxy uses anonymous access (no Docker Hub account needed). You can optionally enable authenticated access for higher rate limits.
The validation blocks are guardrails. If you enable authenticated mode but forget to provide a username or secret, Terraform stops and tells you what is missing instead of creating a broken proxy.
main.tf defines the upstream proxy resource:
What is the dynamic "auth" block? This is a Terraform pattern for conditional configuration. If anonymous mode is on, the auth block is skipped entirely; if it is off, the block includes the Docker Hub credentials. The same configuration file works for both anonymous and authenticated setups. You just flip a variable.
outputs.tf exposes the proxy identifier and URL for later steps:
Set the same environment variables from Step 1 (TF_VAR_harness_account_id, TF_VAR_harness_platform_api_key, TF_VAR_space_ref), then:
For anonymous Docker Hub access (the default), no additional variables are needed. This is fine for pulling public images like nginx, alpine, or redis.
For authenticated Docker Hub access (higher rate limits, private image support), set these additional variables before running terraform apply:
Type yes when prompted. You should see:
Step 3: Link the upstream proxy to the virtual registry
Now connect the two pieces. This step updates the virtual registry from Step 1 to route requests through the upstream proxy from Step 2.
Why is this a separate step? The virtual registry already exists (you created it in Step 1). Step 3 needs to take ownership of that existing resource. In Terraform, this is done with terraform import, a command that tells Terraform "this resource already exists in Harness, start managing it from here."
Create a directory called 03-link-virtual and add the following six files.
versions.tf is the same as previous steps:
providers.tf is the same as previous steps:
data.tf reads the output from Step 2's Terraform state, so this step knows the upstream proxy identifier without you having to type it again.
USE A REMOTE BACKEND IN PRODUCTION
Reading a peer step's local terraform.tfstate only works on a single workstation. For shared use or CI, configure a remote backend (S3, Terraform Cloud, GCS, etc.) on Step 2, then point this terraform_remote_state data source at the same backend instead of a local path. Never commit *.tfstate files to source control.
variables.tf has the same shape as before; virtual_registry_identifier must match Step 1.
main.tf redefines the virtual registry, now with the upstream proxy attached:
The only change from Step 1 is the upstream_proxies line. Instead of an empty list, it now references the proxy identifier from Step 2.
outputs.tf also surfaces the proxies that ended up wired in:
This step requires one extra command compared to the previous steps. Since the virtual registry already exists (created in Step 1), you need to import it so Terraform updates it instead of trying to create a duplicate.
Make sure Step 2 is complete. Its state file must exist:
Set the same environment variables, then initialize:
Import the existing virtual registry. Use the command that Step 1 printed in its output:
Replace the values with your actual account ID, org, project, and registry identifier.
You should see:
Now apply the update:
Type
yes. You should see:0 added, 1 changedmeans Terraform recognized the existing registry and updated it to include the upstream proxy.
Verify the setup
All three steps are complete. Verify the end-to-end flow by pulling a Docker image through your new registry.
Log in to the Harness registry with your Docker client:
You should see
Login Succeeded.Pull an image through the virtual registry:
On the first pull, the request flows through the chain: virtual registry, then upstream proxy, then Docker Hub. The image is cached in Harness. Subsequent pulls are served directly from the cache.
Verify in the Harness UI: navigate to Artifact Registry in your project. You should see:
my-docker listed as a Virtual registry with dockerhub-proxy shown as its upstream.
dockerhub-proxy listed as an Upstream registry connected to Docker Hub.
The
alpineimage visible under the virtual registry's cached artifacts.
If you see all three, your Terraform-provisioned registry is fully operational.
Troubleshooting
Clean up
To remove everything, destroy the resources in reverse order:
Type yes at each prompt.
Destroying a registry permanently deletes all cached artifacts in it. Make sure you have alternative sources for any images before proceeding.
Next steps
You now have a Terraform-managed Docker registry with a Docker Hub upstream proxy. From here, attach more upstream proxies to the same virtual registry, or replicate this pattern for other package types such as Helm, Maven, npm, or PyPI.
Harness Terraform Provider: Full reference for
harness_platform_har_registryand additional configuration options.Create an Artifact Registry (UI): Create registries through the Harness console instead of Terraform.
Create an upstream proxy (UI): Configure upstream proxies through the Harness console.
Artifact Registry overview: Understand the full capabilities of Harness Artifact Registry.
Last updated
Was this helpful?