| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1docker.io 29.1.3-0ubuntu4.1curl 8.18.0git 2.53.0jq 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.
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.
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_datais 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.
sudo apt updateWhen the update finishes without errors, install curl, Git, jq, and unzip:
sudo apt install -y curl git jq unzipConfirm the tools are available:
git --versionSample output:
git version 2.53.0Check jq next:
jq --versionSample output:
jq-1.8.1Finish with curl:
curl --version | head -1Sample 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.10If 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.
sudo apt install -y docker.ioEnable and start the Docker service so containers can run after a reboot:
sudo systemctl enable --now dockerCheck that the client binary is installed:
docker --versionSample output:
Docker version 29.1.3, build 29.1.3-0ubuntu4.1Before your user is in the docker group, use sudo to reach the Docker socket. Run the official hello-world image:
sudo docker run --rm hello-worldSample 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:
sudo docker psSample output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESRun Docker without sudo on every command
Add your login user to the docker group so later Terraform labs do not depend on sudo docker:
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:
idSample output (group ID varies by system):
uid=1001(tflab) gid=1001(tflab) groups=1001(tflab),115(docker)Run hello-world again without sudo to confirm socket access:
docker run --rm hello-worldThe same greeting output means your user can reach the daemon directly.
Verify with a quick container listing:
docker psAn 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:
mkdir -p ~/terraform-labsAdd subdirectories that match how later lessons group exercises:
cd ~/terraform-labs
mkdir getting-started variables providers modules lifecycle state troubleshootingList the layout:
ls ~/terraform-labsSample output:
getting-started lifecycle modules providers state troubleshooting variablesAs you initialize and apply a Terraform configuration, the working directory can contain:
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 changesThe .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 duringterraform init - Do not confuse
.terraform/withterraform.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:
mkdir -p ~/.terraform.d/plugin-cacheAdd a CLI configuration file in your home directory:
cat > ~/.terraformrc <<'EOF'
plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"
EOFEach 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:
cd ~/terraform-labs/getting-startedCreate a minimal configuration:
cat > main.tf <<'EOF'
resource "terraform_data" "lab_test" {
input = "Terraform lab is ready"
}
output "result" {
value = terraform_data.lab_test.input
}
EOFInitialize the working directory:
terraform initSample 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:
terraform fmtterraform fmt exits silently when the file is already formatted.
Validate syntax:
terraform validateSample output:
Success! The configuration is valid.Review the execution plan:
terraform planSample output (trimmed):
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:
terraform applyType yes when prompted, or pass -auto-approve during scripted runs.
Sample output (trimmed):
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:
terraform state listSample output:
terraform_data.lab_testRead the output value:
terraform outputSample output:
result = "Terraform lab is ready"Clean up the smoke test:
terraform destroyApprove 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:
cd ~/terraform-labs/providersCreate a configuration that pulls nginx:alpine and starts one container:
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
}
}
EOFdocker_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:
terraform initSample output (trimmed):
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:
find ~/.terraform.d/plugin-cache -type f -name 'terraform-provider-docker*'Sample output:
/home/<user>/.terraform.d/plugin-cache/registry.terraform.io/kreuzwerker/docker/3.9.0/linux_amd64/terraform-provider-docker_v3.9.0Terraform 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:
terraform validateSample output:
Success! The configuration is valid.Review the plan before applying — you should see one Docker image and one container to be created:
terraform planSample output (trimmed):
Plan: 2 to add, 0 to change, 0 to destroy.Apply the configuration and wait for the container to start:
terraform apply -auto-approveSample output (trimmed):
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:
docker ps --filter name=tf-lab-smokeSample 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-smokeThe port mapping shows host port 8080 forwarded to container port 80.
Confirm the nginx service responds through that mapping:
curl -I http://localhost:8080Sample output:
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/htmlThe 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:
terraform state listSample output:
docker_container.lab
docker_image.nginxDestroy the lab resources:
terraform destroy -auto-approveAfter destroy completes, confirm the container is gone:
docker ps -a --filter name=tf-lab-smokeSample output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESAn 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:
- Run
terraform destroyin the exercise directory when the configuration still exists. - Keep each article or lesson in its own subdirectory under
~/terraform-labs/. - Delete the whole exercise directory only after destroy succeeds and you no longer need the files.
- 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:
terraform versionConfirm Docker client and daemon versions:
docker versionVerify Git is installed:
git --versionConfirm jq is available:
jq --versionYou should also have completed, in ~/terraform-labs/getting-started and ~/terraform-labs/providers:
terraform init
terraform validate
terraform plan
terraform apply
terraform state list
terraform destroywith at least one resource verified outside Terraform (terraform output for the built-in test, docker ps and curl for the container test).
References
- Terraform CLI configuration file
- Terraform init command
- Provider requirements
- Dependency lock file
- terraform_data resource
- Docker provider documentation
- Ubuntu
docker.iopackage - Docker post-installation steps (Linux)
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.tfstatewhile 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.

