Getting Started with Terraform
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
This page gets Terraform onto a workstation and walks through the core workflow once, end to end, on a laptop. It
targets Terraform 1.17 (released 2026-10-21; the previous line is 1.16.x) and the kreuzwerker/docker provider
4.6, and it needs only Docker — no cloud account. For what Terraform is and why it exists, read
Introduction and Infrastructure as Code
first; for the full reading path see the section landing page. The example is
the local slice of the Bookshelf scenario: a network, a web container and a generated database password.
The container side of the story (images, ports, networks) is covered in
Getting Started with Docker and
Docker Networking.
Installing Terraform
Terraform is a single static binary named terraform. HashiCorp documents these installation routes:
| Method | Notes |
|---|---|
Homebrew (macOS, Linux) |
Uses HashiCorp’s own tap, |
Package repositories (Linux) |
|
Chocolatey (Windows) |
|
Manual download |
A zip archive containing the single |
From source |
Clone the repository and run |
Docker image |
|
Distribution packages (for example a distro’s own terraform package) exist but are outside HashiCorp’s
support; prefer one of the routes above or a version manager (next section).
Homebrew
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
terraform version
Source: Install Terraform
To update later, run brew update followed by brew upgrade hashicorp/tap/terraform.
Ubuntu and Debian (apt)
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
Source: Install Terraform
The first command imports HashiCorp’s signing key; the second registers the repository for the distribution’s
codename. After that, apt upgrade delivers new Terraform releases like any other package — fine on a
workstation, but CI should pin an exact version (see the version managers below).
Fedora, RHEL and Amazon Linux (dnf and yum)
# Fedora
wget -O- https://rpm.releases.hashicorp.com/fedora/hashicorp.repo | sudo tee /etc/yum.repos.d/hashicorp.repo
sudo dnf -y install terraform
# RHEL
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install terraform
# Amazon Linux
sudo yum install -y yum-utils shadow-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo yum install terraform
Source: Install Terraform
Windows
The official install page offers the signed zip archives (386, AMD64 and ARM64); the install tutorial additionally
mentions Chocolatey. Either way the result must be a terraform.exe on the PATH:
choco install terraform
terraform version
Source: Install Terraform (tutorial)
For the manual route, download terraform_<version>_windows_amd64.zip from
releases.hashicorp.com, extract terraform.exe into a folder such as
C:\tools\terraform and add that folder to the user Path environment variable. Open a new terminal afterwards:
an already-open shell does not see the changed Path.
The Docker image
HashiCorp publishes a verified image whose entrypoint is the terraform executable, so subcommands are passed as
the container command. Pin a release tag in anything but a quick experiment:
docker run --rm -it hashicorp/terraform:1.17.0 version
# run against the configuration in the current directory
docker run --rm -it \
-v "$PWD":/workspace -w /workspace \
hashicorp/terraform:1.17.0 plan
The container only sees what is mounted and passed in: configuration files via the bind mount, provider
credentials via -e flags or mounted files, and, for the Docker provider used below, the Docker socket. That
makes the image a good fit for pipelines (see
CI/CD with GitHub Actions) and an awkward one for the local
Docker tutorial, so the rest of this page assumes a native install.
Verifying the installation
terraform version
terraform -help
Terraform v1.17.0
on darwin_arm64
terraform -help begins with Usage: terraform [global options] <subcommand> [args]; adding -help to any
subcommand prints its options. The output of terraform version varies with the platform and installed providers
(it lists providers once a directory has been initialized).
Source: Install Terraform (tutorial)
Managing several Terraform versions
A repository’s required_version (see
HCL Syntax and Configuration Files) pins the CLI
versions a configuration accepts, and different projects, or the same project across a minor upgrade, need
different binaries. A version manager installs several versions side by side and picks one per directory.
Three tools are common; all of them read a .terraform-version file, which holds just a version string such as
1.17.0, so the file is the portable way to pin a project.
| Tool | Characteristics |
|---|---|
Written in Go; manages Terraform, OpenTofu, Terragrunt, Terramate and Atmos through proxy binaries. Reads
|
|
A shell-script manager modelled on |
|
A Go binary that offers an interactive version list or switches to a version given as an argument, and also
looks at the |
tenv
brew install tenv # Windows: choco install tenv, scoop install tenv or winget install Tofuutils.Tenv
tenv tf list-remote -s # stable versions that can be installed
tenv tf install 1.17.0
tenv tf use 1.17.0 -w # -w writes .terraform-version in the working directory
tenv tf detect # which version would run here
terraform version # the proxy binary now resolves to 1.17.0
Source: tenv on GitHub
Because tenv also manages OpenTofu (tenv tofu ...), it is the natural choice when a team runs both tools;
OpenTofu explains how they differ.
tfenv
brew install tfenv
tfenv list-remote
tfenv install 1.17.0
tfenv use 1.17.0
tfenv list # installed versions, the active one marked with *
tfenv pin # write the active version to .terraform-version
Source: tfenv on GitHub
With no argument, tfenv install takes the version from TFENV_TERRAFORM_VERSION, then from
.terraform-version, and otherwise uses latest. min-required and latest-allowed scan required_version
in the .tf files, but only look at the first constraint, so a compound constraint can resolve to a surprising
version. Treat them as a convenience and prefer an explicit file.
tfswitch
brew install tfswitch # or: curl -L https://raw.githubusercontent.com/warrensbox/terraform-switcher/master/install.sh | bash
tfswitch # interactive list of versions
tfswitch 1.17.0 # install and switch to a specific version
Source: terraform-switcher on GitHub
tfswitch’s documentation describes the full set of version sources and flags on tfswitch.warrensbox.com; the exact precedence between its sources was not checked for this page, so confirm it there before relying on it in automation.
Choosing a strategy
-
Developers on one project: a
.terraform-versionfile committed to the repository, plus tenv or tfenv. -
CI: install the exact version named in that file (or use the pinned Docker tag) rather than
latest, so a pipeline does not change behaviour on a day nobody touched the code. -
Both Terraform and OpenTofu: tenv, because one tool handles
.terraform-versionand.opentofu-version.
CI/CD and Automation shows the pinned-version pattern in a pipeline.
Editor support
Terraform files are plain text, but an editor that understands HCL catches mistakes before terraform validate
does.
Visual Studio Code
The HashiCorp Terraform extension (ID HashiCorp.terraform) provides syntax highlighting for Terraform 1.x,
validation as you type, and formatting through terraform fmt. It bundles the Terraform language server
(terraform-ls) and keeps it updated, so no separate install is needed. Formatting requires the terraform
binary on the PATH. The extension’s recommended format-on-save setting, repeated for each Terraform language ID
(terraform, terraform-vars, terraform-stack, terraform-test and the others it lists):
{
"[terraform]": {
"editor.defaultFormatter": "hashicorp.terraform",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
}
}
Source: HashiCorp Terraform extension
Enhanced validation (deprecations, missing required attributes) is on by default and controlled by
terraform.validation.enableEnhancedValidation; the extension’s terraform validate integration needs a prior
terraform init. Use terraform.languageServer.terraform.path to point the server at a specific binary, for
example one managed by tenv.
terraform-ls and other editors
terraform-ls is the official language server (LSP) behind the extension. Editors without a bundled integration
use it directly:
brew install hashicorp/tap/terraform-ls
terraform-ls -v
Source: terraform-ls installation
On Linux, terraform-ls is also published in HashiCorp’s apt and RPM repositories under that package name. The
terraform-ls usage guide lists client setup
for Vim and Neovim (coc.nvim, vim-lsp, nvim-lspconfig and others), Emacs (lsp-mode) and Sublime Text
(LSP-terraform).
JetBrains IDEs
JetBrains publishes its own plugin, listed on the Marketplace as Terraform and HCL; its settings page is Tools | Terraform and OpenTofu, where the executable path can be set (the IDE usually detects it) and where run configurations for Terraform are available. The terraform-ls usage guide describes an alternative LSP-based setup for JetBrains IDEs and warns that combining it with the JetBrains plugin can produce duplicate completions, so use one of the two.
Shell completion
Terraform can install tab completion for Bash and Zsh:
touch ~/.zshrc # the shell configuration file must already exist (~/.bashrc for Bash)
terraform -install-autocomplete
exec $SHELL # restart the shell
Source: Install Terraform (tutorial)
The command appends a completion line to the shell’s configuration file; run it once. After that,
terraform pl<TAB> completes to plan, and subcommands list their flags. Completion lives in the CLI binary, so
it follows the version in use; when a version manager switches binaries, the completion keeps working because it
invokes whichever terraform is first on the PATH.
The core workflow on Bookshelf
Terraform’s day-to-day loop is write, initialize, plan, apply; this section runs it once against Docker, following HashiCorp’s Docker get-started tutorial but growing it into the local slice of Bookshelf. You need a running Docker daemon (Docker Desktop, Colima or an engine on Linux). The tutorial’s sequence is: build infrastructure, change infrastructure, destroy infrastructure, define input variables and query data with outputs; the sections below follow it in order.
Write the configuration
Create an empty directory and a file main.tf. The first version creates only the nginx image and container
for the web service, exactly the shape the tutorial uses:
terraform {
required_version = ">= 1.17.0"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 4.6.0"
}
}
}
provider "docker" {}
resource "docker_image" "nginx" {
name = "nginx:1.27"
keep_locally = false
}
resource "docker_container" "web" {
name = "bookshelf-web"
image = docker_image.nginx.image_id
ports {
internal = 80
external = 8000
}
}
What each block does:
-
The
terraformblock declares the CLI versions the configuration accepts (required_version) and where each provider comes from (required_providers).kreuzwerker/dockeris the provider’s source address on the Terraform registry;~> 4.6.0allows patch releases of 4.6 only. The tutorial itself uses~> 4.2.0; the newest release at the time of writing is 4.6.0 and the provider’s README recommends pinning because new versions are released frequently. -
provider "docker" {}configures the provider with defaults, which means it finds the local Docker daemon through the usual environment (for exampleDOCKER_HOST). The tutorial’s Windows variant setshost = "npipe:////.//pipe//docker_engine"instead. -
A
resourceblock has a type (docker_image) and a local name (nginx); together they form the addressdocker_image.nginx. The referencedocker_image.nginx.image_idin the container is an implicit dependency: Terraform builds a graph from such references, so it pulls the image before creating the container and destroys them in the opposite order. -
The image tag is pinned to
nginx:1.27here, where the tutorial usesnginx:latest: a pinned tag keeps the plan reproducible, whilelatestcan change under an unchanged configuration. Replace the tag with whichever release you have verified.
The HCL language itself is the subject of HCL Syntax and Configuration Files; providers are covered in depth in Providers.
terraform init
terraform init
Initializing the backend...
Initializing provider plugins...
- Finding kreuzwerker/docker versions matching "~> 4.6.0"...
- Installing kreuzwerker/docker v4.6.0...
- Installed kreuzwerker/docker v4.6.0 (self-signed, key ID BD080C4571C6104C)
Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/cli/plugins/signing.html
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
Terraform has been successfully initialized!
The output is the tutorial’s, abbreviated; the version numbers follow the pin in this page’s configuration.
init is the first command to run in any directory and it is safe to repeat: it never deletes configuration or
state. It performs three steps: initialize the backend (where state is stored; the default is a local file),
install child modules, and install provider plugins. It must be re-run whenever the backend configuration,
a module source or the provider requirements change; other commands detect a stale directory and say so.
Useful options:
-
-upgrade— ignore the lock file and select the newest provider versions that satisfy the constraints; for modules, update to the newest matching source. -
-backend=false— skip backend initialization, as used byterraform validatein CI (below). -
-reconfigure— use new backend settings and ignore the previous ones without migrating state. -
-migrate-state— copy existing state to a newly configured backend. -
-input=false— fail instead of prompting, for automation.
Source: terraform init
What init creates
After init the directory contains two new items:
.
├── .terraform/ # working data: downloaded providers (and modules), backend settings
├── .terraform.lock.hcl # the dependency lock file
└── main.tf
-
.terraform/is a local cache that Terraform writes to the current working directory (the environment variableTF_DATA_DIRrelocates it). It holds the downloaded provider binaries, installed modules and the recorded backend configuration. It is machine-specific and fully reproducible withterraform init, so it is never committed. -
.terraform.lock.hclrecords the exact provider version selected, the constraint it satisfied and the checksums (hashes) that downloaded packages must match. It lives next to the root module’s.tffiles and applies to the whole configuration. HashiCorp’s guidance is to include it in version control, so that teammates and CI receive the same provider builds and changes to dependencies go through code review. It is covered in depth, includingproviders lockfor several platforms, in Providers.
An excerpt, using the shape documented by HashiCorp (hashes shortened):
provider "registry.terraform.io/kreuzwerker/docker" {
version = "4.6.0"
constraints = "~> 4.6.0"
hashes = [
"h1:...",
"zh:...",
]
}
Source: Dependency Lock File
terraform fmt and terraform validate
terraform fmt
terraform validate
Success! The configuration is valid.
terraform fmt rewrites the files in the current directory into the canonical style (indentation, aligned =
signs, spacing) and lists the files it changed. Useful options are -recursive to include subdirectories, -diff
to show what would change, and -check to change nothing and exit non-zero if any file needs formatting, which is
how a CI job enforces the style. terraform validate checks syntax and internal consistency — attribute names,
value types, references — regardless of variable values or state. It needs an initialized directory (use
terraform init -backend=false to initialize without touching a backend) and does not contact remote services or
provider APIs; -json produces machine-readable diagnostics for editors and pipelines.
Sources: terraform fmt and terraform validate
terraform plan
terraform plan
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# docker_container.web will be created
+ resource "docker_container" "web" {
+ image = (known after apply)
+ name = "bookshelf-web"
...
+ ports {
+ external = 8000
+ internal = 80
+ ip = "0.0.0.0"
+ protocol = "tcp"
}
}
# docker_image.nginx will be created
+ resource "docker_image" "nginx" {
+ id = (known after apply)
+ keep_locally = false
+ name = "nginx:1.27"
...
}
Plan: 2 to add, 0 to change, 0 to destroy.
plan compares three things — the configuration, the state, and (after refreshing it) the real objects — and
prints the actions needed to make reality match the configuration. It changes nothing. The output is abbreviated
here (... marks omitted attributes) and the attribute list varies with the provider version.
A value shown as (known after apply) is computed by the provider when the object is created and cannot be known
in advance.
Reading plan output symbols
The legend printed at the top of every plan lists the actions in use. The symbols are:
| Symbol | Meaning |
|---|---|
|
Create: the object does not exist yet. |
|
Destroy: the object will be removed. |
|
Update in place: an argument changes without destroying the object. |
|
Replace, destroy then create: the change cannot be made in place. The attribute that forces it is annotated
|
|
Replace, create then destroy: the order used when a resource has |
|
Read: a data source is read. When its arguments are not known until apply, the plan shows the read as deferred to apply time. |
Source: terraform plan
The plan reference documents +, -, ~ and -/+. The +/- and <= rows come from the legend Terraform prints
at the top of a plan and from the data source documentation, which only states that deferred reads are indicated
in the plan; check the legend of your own plan output, since the exact wording is the CLI’s to define. Reading a
plan is a skill of its own: Dependency Graph and Plan
covers planning modes, saved plans and the situations that produce surprising replacements. The rule of thumb is:
read every - and every -/+ line before approving, because those are the lines that delete data.
terraform apply
terraform apply
Plan: 2 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
docker_image.nginx: Creating...
docker_image.nginx: Creation complete after 13s [id=sha256:...nginx:1.27]
docker_container.web: Creating...
docker_container.web: Creation complete after 2s [id=...]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Without a plan file, apply creates a plan, shows it and waits for the literal answer yes. Open
http://localhost:8000 to see the nginx welcome page, and docker ps shows the container named
bookshelf-web. Options worth knowing now:
-
-auto-approve— skip the prompt. Intended for automation, where a reviewed plan has already been approved; it also approves destructive changes, so avoid it at a keyboard. -
A saved plan file as argument (
terraform apply tfplan) applies exactly that plan without prompting; planning options cannot be combined with it. -
-input=false— never prompt. Because the approval prompt is also an input,applythen fails rather than proceeding unless-auto-approveor a plan file is given. -
-var/-var-file— set input variables (below).
Source: terraform apply
terraform show
terraform show
terraform show -json | head
terraform show prints the latest state snapshot in human-readable form; given a saved plan file it prints that
plan instead. -json produces the machine-readable form, in which sensitive values appear in plain text, so
treat that output as sensitive. The state itself — the file terraform.tfstate that apply created next to the
configuration — is what Terraform uses to map configuration addresses to real objects; it is explained in
State.
Source: terraform show
Changing infrastructure
Edit main.tf and change the published port from 8000 to 8080, then run terraform plan. The tutorial shows
what happens:
# docker_container.web must be replaced
-/+ resource "docker_container" "web" {
...
~ ports {
~ external = 8000 -> 8080 # forces replacement
# (3 unchanged attributes hidden)
}
}
Plan: 1 to add, 0 to change, 1 to destroy.
The excerpt is abbreviated: the tutorial documents the -/+ marker, the # forces replacement annotation on the
external port and the Plan: 1 to add, 0 to change, 1 to destroy. summary; the surrounding attributes vary with the
provider version. The container’s port mapping cannot be altered in place, so the provider declares the argument as
forcing replacement: Terraform destroys the old container and creates a new one, and apply ends with
Apply complete! Resources: 1 added, 0 changed, 1 destroyed. This is the most important lesson of the first hour:
whether a change is an update or a replacement is a property of the provider’s schema, not of Terraform core,
and the plan is where you learn it.
Input variables and outputs
Hard-coded values do not scale, so the next step in the tutorial extracts them. Add variables.tf and
outputs.tf, with the Bookshelf additions: a generated name suffix and a generated database password.
# variables.tf
variable "container_name" {
description = "Name of the Docker container"
type = string
default = "bookshelf-web"
}
variable "web_port" {
description = "Host port published for the web container"
type = number
default = 8080
}
# main.tf (additions and changes)
terraform {
required_version = ">= 1.17.0"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 4.6.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.9"
}
}
}
provider "docker" {}
resource "random_pet" "suffix" {
length = 2
}
resource "random_password" "postgres" {
length = 24
special = false
}
resource "docker_image" "nginx" {
name = "nginx:1.27"
keep_locally = false
}
resource "docker_container" "web" {
name = "${var.container_name}-${random_pet.suffix.id}"
image = docker_image.nginx.image_id
ports {
internal = 80
external = var.web_port
}
}
# outputs.tf
output "container_id" {
description = "ID of the Docker container"
value = docker_container.web.id
}
output "image_id" {
description = "ID of the Docker image"
value = docker_image.nginx.id
}
output "web_url" {
description = "Where the web container is reachable"
value = "http://localhost:${var.web_port}"
}
output "postgres_password" {
description = "Generated database password (for the next stages)"
value = random_password.postgres.result
sensitive = true
}
The tutorial’s variable is container_name with a description, type and default, and its outputs are
container_id and image_id; the random resources, web_port, web_url and postgres_password are
additions for Bookshelf. random_pet generates a readable name (two words separated by - by default) and
random_password generates a password with a cryptographically secure generator. Its result is marked
sensitive, so it is hidden in console output, but it is still stored in plain text in the state file — one of
the reasons state must be treated as a secret (see State and
Sensitive Data, Ephemeral Values and Write-only Arguments).
Variables can be overridden without editing files. Run terraform init again (a new provider was added), then:
terraform init
terraform apply -var 'web_port=9090'
terraform output
terraform output web_url
terraform output -raw postgres_password
container_id = "e5fff27c62e2..."
image_id = "sha256:d1a364dc548d...nginx:1.27"
postgres_password = <sensitive>
web_url = "http://localhost:9090"
terraform output lists all outputs of the root module; naming one prints that value, and a sensitive value is
masked as <sensitive> unless it is requested by name (for example with -raw). Outputs are also the
interface between modules and between configurations, which is why they get a page of their own:
Variables, Outputs and Locals covers precedence
(terraform.tfvars, -var-file, TF_VAR_ environment variables), validation, and sensitivity.
terraform destroy
terraform destroy
Plan: 0 to add, 0 to change, 2 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
docker_container.web: Destroying...
docker_container.web: Destruction complete after 2s
docker_image.nginx: Destroying...
docker_image.nginx: Destruction complete after 0s
Destroy complete! Resources: 2 destroyed.
destroy plans the removal of every object recorded in the current state and, after confirmation, deletes them in
reverse dependency order: the container first, then the image it uses. It only affects the resources this
configuration manages. With the Bookshelf additions the plan reports the random_* resources as well (they only
exist in state, so their removal is instant). terraform apply -destroy creates the same plan; both honour
-auto-approve. Destroy is the reason throwaway environments are cheap with Terraform, and the reason that
production resources should carry prevent_destroy guards, covered in
Lifecycle, Validation and Checks.
Files to commit and files to ignore
A Terraform working directory mixes configuration that belongs in version control with local state and cache that
must stay out. The community .gitignore template maintained on GitHub ignores local data directories, state
files, crash logs, variable files that may hold secrets, override files and CLI configuration. An adapted version
for Bookshelf:
# Local provider and module cache
.terraform/
# State files: never commit, they can contain secrets in plain text
*.tfstate
*.tfstate.*
# Crash logs
crash.log
crash.*.log
# Variable files often hold secrets; commit only deliberate, secret-free ones
*.tfvars
*.tfvars.json
!example.tfvars
# Local override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Saved plan files and graph output
tfplan
*.tfplan
*.dot
# CLI configuration
.terraformrc
terraform.rc
Note what is deliberately not ignored: .terraform.lock.hcl is committed (see above), as are the .tf files
and any secret-free .tfvars example. If *.tfvars are ignored, keep an example.tfvars showing the expected
variables. Once a remote backend is in use (see State), no state file is
created in the repository at all, but the ignore rules stay as a safety net. Before the first commit, run
terraform fmt -check and terraform validate; CI can repeat both, as shown in
CI/CD and Automation.
Command summary
| Command | Purpose |
|---|---|
|
Initialize the backend, install modules and providers, create or update the lock file. |
|
Rewrite files into canonical style; |
|
Check syntax and internal consistency without contacting remote services. |
|
Show the actions needed to match configuration; changes nothing. |
|
Plan, prompt, then perform the actions; |
|
Print the state, or a saved plan. |
|
Print root module outputs. |
|
Plan and perform the removal of everything in state. |
Source: Terraform CLI documentation
The remaining commands, global options and environment variables are collected in CLI Essentials.
What changed recently
-
Terraform 1.17 is the current release as of this writing (2026-10-21); install pages and tutorials show it as the current version.
-
The Docker provider is now in its 4.x line (4.0.0 was a major release with a documented migration guide; the Docker tutorial still pins
~> 4.2.0). Pin it explicitly, as above. -
Older books and tutorials show
docker_image.nginx.latestas the container’s image reference; the current tutorial usesdocker_image.nginx.image_id. -
The legacy HashiCorp
aptrepository addresses and manualgpgsteps in older material have been replaced by the commands on the current install page. -
Where books describe installation as "download a zip and add it to the PATH", that route is still valid, but the package-manager routes and a version manager are now the usual practice.