Skip to main content

[Terraform] 1 - Build Infrastructure

First entry in a running series of Terraform labs — the basics, worked through hands-on rather than just read about. This one covers the full loop for a single resource: write config, initialize, validate, plan, apply, then inspect what Terraform actually did to state.

Prerequisites

A working directory to hold the config, kept separate from anything else:

mkdir learn-terraform-docker-container && cd learn-terraform-docker-container

And a file to define the infrastructure in:

touch main.tf

I opened main.tf and dropped in this configuration — a Docker provider, an nginx image, and a container exposing it on port 8000:

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0.1"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name         = "nginx:latest"
  keep_locally = false
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.image_id
  name  = "tutorial"
  ports {
    internal = 80
    external = 8000
  }
}

What each block is doing:

terraform {                            # settings block - which providers
  required_providers {                 #   Terraform needs to provision
    docker = {                         #   this config, and which version
      source  = "kreuzwerker/docker"
      version = "~> 3.0.1"
    }
  }
}

provider "docker" {}                   # configures the docker provider itself

resource "docker_image" "nginx" {      # pulls the nginx:latest image
  name         = "nginx:latest"
  keep_locally = false                 #   don't keep it locally after destroy
}

resource "docker_container" "nginx" {  # runs a container from that image
  image = docker_image.nginx.image_id
  name  = "tutorial"
  ports {
    internal = 80                      # container's internal port
    external = 8000                    # exposed on the host as :8000
  }
}

Initialize the directory

Any new config directory — or one freshly checked out from version control — needs terraform init before anything else works. It reads the required_providers block, downloads the matching provider plugin (docker, in this case), and sets up the local .terraform/ directory:

terraform init

Screenshot: terraform init output showing the docker provider being installed.

Format and validate the configuration

terraform fmt rewrites the config files in the current directory into Terraform’s canonical formatting — consistent indentation and alignment, useful to run before every commit:

terraform fmt

terraform validate catches syntax errors and internal inconsistencies (referencing a resource that doesn’t exist, wrong argument types, etc.) without needing any provider credentials or touching real infrastructure:

terraform validate

Screenshot: terraform validate confirming the configuration is valid.

Worth running both as a matter of habit — fmt before every commit, validate before every plan, especially once a config has more than one or two resources.

Checking the changes

terraform plan

Screenshot: terraform plan output showing the two resources to be created.

plan is a dry run — it shows exactly what Terraform intends to create, change, or destroy without touching anything yet. Always worth reading before an apply, even for something this small; the habit matters more once configs get larger and plans get longer.

Create infrastructure

terraform apply

Screenshot: terraform apply output (truncated) after confirming with yes.

After confirming, Terraform provisions both resources — pulls the image, then starts the container, mapped to localhost:8000.

Inspect state

Every apply writes the IDs and current properties of everything it manages into terraform.tfstate — that’s how it knows what to update or destroy on future runs, without re-querying every resource from scratch each time.

terraform show prints the current state in a human-readable form:

terraform show

Screenshot: terraform show output for the docker_image and docker_container resources.

Outside Terraform entirely, docker ps confirms the container is actually up and reachable:

docker ps

Screenshot: docker ps showing the tutorial container running and mapped to port 8000.

Working with state directly

terraform state is the built-in toolkit for more advanced state operations. list, the most basic subcommand, prints every resource currently tracked in state:

terraform state list

Screenshot: terraform state list showing docker_image.nginx and docker_container.nginx.


Next in this series: modifying this same config and watching Terraform work out an in-place update vs. a replace.