Set Up a Terraform Lab on Ubuntu with Docker

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
docker.io 29.1.3-0ubuntu4.1
curl 8.18.0
git 2.53.0
jq 1.8.1
Applies to Ubuntu
Lab environment Single Ubuntu 26.04 VM with Docker Engine — this guide establishes the lab
Privilege sudo or root
Scope One-VM Terraform local lab on Ubuntu — helper packages, Docker Engine, per-exercise directories, optional plugin cache, and smoke tests with built-in and Docker providers. Does not install Terraform, teach HCL, remote state, or cloud credentials.
Related guides apt command
sudo command
check Ubuntu version
What is Terraform?

This walkthrough prepares a reusable Terraform local lab on one Ubuntu machine. This Terraform local lab uses Docker so you can practice the full Terraform workflow without a cloud account.

You will:

  • Install Docker and a few CLI helpers
  • Create separate working directories for later lessons
  • Run two smoke tests — one without downloading a provider, and one that creates a real container you can inspect with docker ps

You do not need AWS, Azure, or GCP credentials. Terraform installation is covered separately; confirm terraform version works before you start here.

IMPORTANT
This article sets up the lab environment only. It does not install Terraform from the HashiCorp apt repository, teach HCL syntax, or configure remote state backends.

Terraform lab architecture

Terraform runs on your Ubuntu VM. The CLI reads configuration and state files, then calls provider plugins to make changes. In this guide you use the built-in terraform provider for a quick smoke test and the Docker provider to manage a container on the same host.

text
Terraform CLI
     ├── Built-in terraform provider
     │      └── terraform_data (smoke test in this guide)
     └── Docker provider (kreuzwerker/docker)
        Docker Engine
            └── Container (verified with docker ps / curl)

Later lessons in this course also use providers such as hashicorp/local and hashicorp/random for filesystem and naming exercises.

Docker is part of this lab because it gives Terraform real external resources you can verify independently — not only built-in terraform_data records:

  • terraform_data is built into Terraform and does not manage infrastructure outside Terraform itself
  • The Docker provider talks to the Docker API on your VM to create resources you can inspect with standard Docker commands

Terraform lab requirements

Component Recommendation
VMs 1
OS Ubuntu 26.04 LTS (Ubuntu 24.04 LTS uses the same steps)
CPU 2 vCPU
RAM 4 GiB minimum; 8 GiB recommended
Disk 20–30 GiB free
Network Outbound HTTPS for provider and image downloads
Privileges sudo for package install and Docker setup
Terraform Installed and on PATH
Docker Required for container-based labs
Git Recommended for module and Registry examples later
jq Recommended for parsing JSON output in scripts
Graphviz Install later when a lesson needs terraform graph

Extra CPU or RAM is not required for Terraform alone — it mainly helps when Docker pulls images and several lab directories keep provider caches on disk.

You only need a working Ubuntu VM with internet access. Hypervisor networking modes (NAT, host-only, bridged) are outside this guide as long as the VM can reach HTTPS endpoints such as registry.terraform.io.


Prepare the Ubuntu VM

Refresh package indexes and install helper tools you will reuse across lab exercises. This step does not install Terraform.

bash
sudo apt update

When the update finishes without errors, install curl, Git, jq, and unzip:

bash
sudo apt install -y curl git jq unzip

Confirm the tools are available:

bash
git --version

Sample output:

output
git version 2.53.0

Check jq next:

bash
jq --version

Sample output:

output
jq-1.8.1

Finish with curl:

bash
curl --version | head -1

Sample output:

output
curl 8.18.0 (x86_64-pc-linux-gnu) libcurl/8.18.0 OpenSSL/3.5.5 zlib/1.3.1 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.2 libssh2/1.11.1 nghttp2/1.68.0 librtmp/2.3 mit-krb5/1.22.1 OpenLDAP/2.6.10

If terraform version is not found, install Terraform first using the dedicated Ubuntu install guide in this course series, then return here.


Install and verify Docker for Terraform labs

Install Docker from Ubuntu's docker.io package. This lab uses the distribution-maintained package rather than Docker's upstream docker-ce repository — only a working local Docker API is required for Terraform exercises.

bash
sudo apt install -y docker.io

Enable and start the Docker service so containers can run after a reboot:

bash
sudo systemctl enable --now docker

Check that the client binary is installed:

bash
docker --version

Sample output:

output
Docker version 29.1.3, build 29.1.3-0ubuntu4.1

Before your user is in the docker group, use sudo to reach the Docker socket. Run the official hello-world image:

bash
sudo docker run --rm hello-world

Sample output:

output
Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

The greeting means the daemon, image pull, and container runtime are working.

List running containers with sudo — the list should be empty right after hello-world exits:

bash
sudo docker ps

Sample output:

output
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Run Docker without sudo on every command

Add your login user to the docker group so later Terraform labs do not depend on sudo docker:

bash
sudo usermod -aG docker "$USER"

Group membership is not active in the current shell until you log out and back in, or run newgrp docker.

After a new login session, confirm the docker group appears in your groups:

bash
id

Sample output (group ID varies by system):

output
uid=1001(tflab) gid=1001(tflab) groups=1001(tflab),115(docker)

Run hello-world again without sudo to confirm socket access:

bash
docker run --rm hello-world

The same greeting output means your user can reach the daemon directly.

Verify with a quick container listing:

bash
docker ps

An empty table with no permission error means the socket permissions are correct.


Create a Terraform lab directory structure

Each Terraform configuration should live in its own directory with its own state file. Do not reuse one terraform.tfstate across unrelated examples.

Create the top-level lab tree:

bash
mkdir -p ~/terraform-labs

Add subdirectories that match how later lessons group exercises:

bash
cd ~/terraform-labs
mkdir getting-started variables providers modules lifecycle state troubleshooting

List the layout:

bash
ls ~/terraform-labs

Sample output:

output
getting-started  lifecycle  modules  providers  state  troubleshooting  variables

As you initialize and apply a Terraform configuration, the working directory can contain:

text
main.tf
.terraform/              # init metadata, provider binaries, and module cache for this directory
.terraform.lock.hcl      # provider version selections and checksums from terraform init
terraform.tfstate        # current resource mapping for this directory
terraform.tfstate.backup # previous state snapshot after changes

The .terraform.lock.hcl file records the provider selections and checksums chosen by terraform init, helping later runs install consistent provider packages. Preserve it in version control for predictable provider installation — do not treat it like disposable cache data.

  • You can delete .terraform/ when you want to reinitialize the working directory — Terraform recreates it during terraform init
  • Do not confuse .terraform/ with terraform.tfstate: deleting the state file can cause Terraform to lose track of managed resources
  • Removing .terraform/ only removes local init metadata and provider copies for that directory

Configure a Terraform plugin cache (optional)

When you maintain many lab directories, Terraform downloads providers into each .terraform/ folder by default. A global plugin cache shares downloaded provider binaries across directories.

Create the cache directory — Terraform does not create it for you:

bash
mkdir -p ~/.terraform.d/plugin-cache

Add a CLI configuration file in your home directory:

bash
cat > ~/.terraformrc <<'EOF'
plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"
EOF

Each working directory still keeps its own .terraform/ metadata and lock file. The cache only reduces repeated downloads when you terraform init in a new folder.

  • Terraform does not automatically remove old provider versions from this cache
  • The cache can grow over time as later labs use newer versions

You will verify the cache after terraform init in the Docker provider test later in this guide.


Test Terraform without a cloud account

Use the built-in terraform_data resource first. It does not download an external provider, so it is a fast check that the CLI and state workflow work.

Move into the getting-started directory:

bash
cd ~/terraform-labs/getting-started

Create a minimal configuration:

bash
cat > main.tf <<'EOF'
resource "terraform_data" "lab_test" {
  input = "Terraform lab is ready"
}

output "result" {
  value = terraform_data.lab_test.input
}
EOF

Initialize the working directory:

bash
terraform init

Sample output:

output
Initializing the backend...

Initializing provider plugins...
- terraform.io/builtin/terraform is built in to Terraform

Terraform has been successfully initialized!

Format and validate the file:

bash
terraform fmt

terraform fmt exits silently when the file is already formatted.

Validate syntax:

bash
terraform validate

Sample output:

output
Success! The configuration is valid.

Review the execution plan:

bash
terraform plan

Sample output (trimmed):

output
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

  # terraform_data.lab_test will be created
  + resource "terraform_data" "lab_test" {
      + id     = (known after apply)
      + input  = "Terraform lab is ready"
      + output = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + result = "Terraform lab is ready"

Apply the plan:

bash
terraform apply

Type yes when prompted, or pass -auto-approve during scripted runs.

Sample output (trimmed):

output
terraform_data.lab_test: Creating...
terraform_data.lab_test: Creation complete after 0s [id=bfbfa6a2-b126-c21c-cb81-7ec171b48b44]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

result = "Terraform lab is ready"

Confirm Terraform recorded the resource in state:

bash
terraform state list

Sample output:

output
terraform_data.lab_test

Read the output value:

bash
terraform output

Sample output:

output
result = "Terraform lab is ready"

Clean up the smoke test:

bash
terraform destroy

Approve with yes when prompted. The destroy plan should remove terraform_data.lab_test and clear the output.

This cycle proves the core workflow on your VM before any cloud or Docker provider is involved:

  • init, plan, apply, state, and destroy all work
  • No external provider download was required

Test Terraform with Docker

The Docker provider test confirms Terraform can manage a real container you can inspect outside Terraform.

Switch to the providers lab directory:

bash
cd ~/terraform-labs/providers

Create a configuration that pulls nginx:alpine and starts one container:

bash
cat > main.tf <<'EOF'
terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name = "nginx:alpine"
}

resource "docker_container" "lab" {
  name  = "tf-lab-smoke"
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = 8080
  }
}
EOF

docker_container.lab references docker_image.nginx.image_id, so Terraform automatically understands that the image must exist before it creates the container.

Download the Docker provider plugin:

bash
terraform init

Sample output (trimmed):

output
Initializing provider plugins...
- Finding kreuzwerker/docker versions matching "~> 3.0"...
- Installing kreuzwerker/docker v3.9.0...
- Installed kreuzwerker/docker v3.9.0 (self-signed, key ID 0DCE698927DAF8EC)

Terraform has been successfully initialized!

If you configured the optional plugin cache, confirm the Docker provider binary was stored there:

bash
find ~/.terraform.d/plugin-cache -type f -name 'terraform-provider-docker*'

Sample output:

output
/home/<user>/.terraform.d/plugin-cache/registry.terraform.io/kreuzwerker/docker/3.9.0/linux_amd64/terraform-provider-docker_v3.9.0

Terraform checks this shared cache during terraform init, downloads a missing provider into it when needed, and then installs or reuses the provider in the working directory.

Validate the configuration first:

bash
terraform validate

Sample output:

output
Success! The configuration is valid.

Review the plan before applying — you should see one Docker image and one container to be created:

bash
terraform plan

Sample output (trimmed):

output
Plan: 2 to add, 0 to change, 0 to destroy.

Apply the configuration and wait for the container to start:

bash
terraform apply -auto-approve

Sample output (trimmed):

output
docker_image.nginx: Creating...
docker_image.nginx: Creation complete after 0s [id=sha256:...nginx:alpine]
docker_container.lab: Creating...
docker_container.lab: Creation complete after 2s [id=195bff57b5c2...]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Verify the container exists independently of Terraform:

bash
docker ps --filter name=tf-lab-smoke

Sample output:

output
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                            NAMES
195bff57b5c2   sha256:334d9   "/docker-entrypoint.…"   2 seconds ago   Up 1 second    0.0.0.0:8080->80/tcp             tf-lab-smoke

The port mapping shows host port 8080 forwarded to container port 80.

Confirm the nginx service responds through that mapping:

bash
curl -I http://localhost:8080

Sample output:

output
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/html

The HTTP response confirms that Terraform did not merely create a Docker object — the nginx container is running and its port mapping is usable.

Terraform configuration flowed through the Docker provider to the Docker API, and the container appears in docker ps.

Confirm both resources are tracked in state:

bash
terraform state list

Sample output:

output
docker_container.lab
docker_image.nginx

Destroy the lab resources:

bash
terraform destroy -auto-approve

After destroy completes, confirm the container is gone:

bash
docker ps -a --filter name=tf-lab-smoke

Sample output:

output
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

An empty table means Terraform removed the managed container.


Reset the lab between Terraform exercises

Use this order when you finish an exercise or want a clean slate:

  1. Run terraform destroy in the exercise directory when the configuration still exists.
  2. Keep each article or lesson in its own subdirectory under ~/terraform-labs/.
  3. Delete the whole exercise directory only after destroy succeeds and you no longer need the files.
  4. Take a VM snapshot before experiments that intentionally break state or configuration.

You may delete .terraform/ when you want to force a fresh terraform init in that directory. Do not delete terraform.tfstate while managed resources still exist — Terraform loses its map to real infrastructure and later runs may try to recreate resources that are still running on the host.


Common Terraform lab setup problems

Symptom Likely cause Fix
terraform: command not found Terraform not installed or not on PATH Install Terraform from the Ubuntu install guide; open a new shell
permission denied connecting to Docker User not in docker group sudo usermod -aG docker "$USER", then log out and back in
terraform init cannot download providers No HTTPS to registry.terraform.io Test with curl -I https://registry.terraform.io/; fix DNS, proxy, or firewall
Plugin cache errors Cache directory missing mkdir -p ~/.terraform.d/plugin-cache
Docker provider cannot connect Docker daemon not running sudo systemctl status docker; start with sudo systemctl start docker
docker run hello-world fails to pull No route to Docker Hub or rate limiting Use sudo docker run --rm hello-world before joining the docker group; confirm outbound HTTPS; retry later if rate limited

Provider plan/apply errors, state corruption, and dependency cycles belong in the dedicated Terraform troubleshooting guide — not in this lab setup article.


Lab ready checklist

Before you open the next lesson, confirm these commands succeed.

Check the Terraform CLI:

bash
terraform version

Confirm Docker client and daemon versions:

bash
docker version

Verify Git is installed:

bash
git --version

Confirm jq is available:

bash
jq --version

You should also have completed, in ~/terraform-labs/getting-started and ~/terraform-labs/providers:

text
terraform init
terraform validate
terraform plan
terraform apply
terraform state list
terraform destroy

with at least one resource verified outside Terraform (terraform output for the built-in test, docker ps and curl for the container test).


References


Summary

You now have a single Ubuntu VM prepared for Terraform practice:

  • Helper CLI tools and Docker Engine installed
  • Isolated directories under ~/terraform-labs/
  • Optional global plugin cache configured

The built-in terraform_data smoke test confirmed the core workflow without downloading a provider. The Docker provider test showed Terraform creating a container you could verify with docker ps and curl before terraform destroy removed it.

The main habit to keep is one working directory per exercise with its own state file:

  • Resets stay predictable
  • Later lessons on variables, modules, and remote state do not step on each other
  • You can delete .terraform/ to reinitialize a directory
  • Never delete terraform.tfstate while resources still exist on the host

Install Terraform from the HashiCorp apt repository if you have not already, then continue with HCL syntax and individual CLI command guides in this course track.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)