terraform destroy Command with Examples

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
kreuzwerker/docker 3.9.0
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform and Docker — Terraform lab environment on Ubuntu
Privilege Normal user (Docker socket access required)
Scope terraform destroy workflow — disposable Docker lab, plan -destroy preview, confirmation prompt, -auto-approve, -var and -var-file, -target with warnings, remove-from-config comparison, state after destroy, and brief prevent_destroy. Does not cover full lifecycle rules, state rm shortcuts, backend deletion, or cloud account teardown.
Related guides Terraform lab environment on Ubuntu
terraform plan command
Terraform init command
Terraform providers
Terraform Associate certification course

terraform destroy tears down infrastructure Terraform manages in the current working directory. This guide provisions disposable Docker resources, verifies them outside Terraform, previews deletion, walks through confirmation and automation flags, and shows what remains in state and on disk afterward.

NOTE
Use the Terraform lab environment on Ubuntu with Docker Engine running. Work in ~/terraform-labs/terraform-destroy/ so you do not collide with other lessons on the same VM.

What does terraform destroy do?

terraform destroy builds a destroy plan for every resource instance recorded in the current state, shows you the proposed deletions, and — after confirmation unless you pass -auto-approve — deprovisions those remote objects through provider APIs.

In current Terraform releases, terraform destroy is effectively destroy-mode apply. The same graph walk that creates and updates resources during apply is reused to delete them. You do not need a separate legacy destroy engine — one workflow handles the full lifecycle.

Destroy only affects objects managed by this configuration and state. It does not remove unrelated Docker containers, networks, or volumes you created manually outside Terraform.


Prepare disposable Terraform resources

Provision real objects first so destroy has something to remove and you can verify results with Docker commands.

Create the lab directory:

bash
mkdir -p ~/terraform-labs/terraform-destroy

Move into it — every command below assumes you are here:

bash
cd ~/terraform-labs/terraform-destroy

Write a small stack with a network, image, container, and volume. This lab uses a cached ghcr.io/nginx/nginx-unprivileged:alpine image so you are not blocked when Docker Hub rate-limits anonymous pulls:

hcl
terraform {
  required_version = ">= 1.12.0"

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

provider "docker" {}

variable "environment" {
  type    = string
  default = "lab"
}

resource "docker_network" "lab" {
  name = "tf-destroy-lab"
}

resource "docker_image" "nginx" {
  name         = "ghcr.io/nginx/nginx-unprivileged:alpine"
  keep_locally = true
}

resource "docker_container" "web" {
  name  = "tf-destroy-web"
  image = docker_image.nginx.image_id

  networks_advanced {
    name = docker_network.lab.name
  }

  ports {
    internal = 8080
    external = 8092
  }

  labels {
    label = "environment"
    value = var.environment
  }
}

resource "docker_volume" "data" {
  name = "tf-destroy-data"
}

Save that as main.tf, then initialize the working directory:

bash
terraform init

Create the Docker resources Terraform will manage:

bash
terraform apply

Type yes when prompted, or pass -auto-approve in the lab. Confirm the resources exist outside Terraform:

bash
docker ps --filter name=tf-destroy-web

Sample output:

output
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                    NAMES
05172779525b   334d92979f15   "/docker-entrypoint.…"   12 seconds ago   Up 11 seconds   0.0.0.0:8092->8080/tcp   tf-destroy-web

The container is running on port 8092. List the custom network next:

bash
docker network ls --filter name=tf-destroy-lab

Sample output:

output
NETWORK ID     NAME             DRIVER    SCOPE
335e8514b8ac   tf-destroy-lab   bridge    local

Confirm the named volume exists as well:

bash
docker volume ls --filter name=tf-destroy-data

Sample output:

output
DRIVER    VOLUME NAME
local     tf-destroy-data

Four managed objects now exist in both Docker and Terraform state — a realistic starting point for teardown. After destroy, Terraform removes all four resource instances from state, but keep_locally = true on the image means Docker may still keep that image on disk even when docker_image.nginx is gone from state.


Preview destruction before running terraform destroy

Always review what Terraform will delete. terraform plan -destroy prints the same destroy diff without executing it — see terraform plan command for full plan-reading detail.

bash
terraform plan -destroy

Sample output (trimmed):

output
# docker_container.web will be destroyed
  - resource "docker_container" "web" { ... }

  # docker_image.nginx will be destroyed
  - resource "docker_image" "nginx" { ... }

  # docker_network.lab will be destroyed
  - resource "docker_network" "lab" { ... }

  # docker_volume.data will be destroyed
  - resource "docker_volume" "data" { ... }

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

The - symbols and the summary line tell you Terraform intends to delete every managed instance. A speculative terraform plan -destroy is only a preview — run terraform destroy when you are ready to execute that teardown.


Destroy all managed resources

Run destroy when the preview matches what you expect:

bash
terraform destroy

Terraform prints the destroy plan again, then asks for explicit confirmation:

output
Plan: 0 to add, 0 to change, 4 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:

Decline once in the lab to see the safe default — type anything other than yes:

bash
printf 'no\n' | terraform destroy

Sample output:

output
Destroy cancelled.

Nothing is deleted and state is unchanged. When you are ready to proceed, approve the run:

bash
terraform destroy -auto-approve

Sample output (trimmed):

output
docker_container.web: Destroying...
docker_container.web: Destruction complete after 1s
docker_network.lab: Destroying...
docker_volume.data: Destroying...
docker_image.nginx: Destroying...

Destroy complete! Resources: 4 destroyed.

Verify resources were destroyed

Terraform destroys four resource instances from state. On the host, only three Docker objects disappear — the container, network, and volume. The image stays in Docker's local image store because keep_locally = true on docker_image.nginx.

Check the container first:

bash
docker ps -a --filter name=tf-destroy-web

The container name should no longer appear. Check the network next:

bash
docker network ls --filter name=tf-destroy-lab

Check the volume:

bash
docker volume ls --filter name=tf-destroy-data

Those three commands should return no matching rows. Inspect the image Terraform tracked:

bash
docker image inspect ghcr.io/nginx/nginx-unprivileged:alpine

Sample output (trimmed):

output
[
    {
        "Id": "sha256:...",
        "RepoTags": [
            "ghcr.io/nginx/nginx-unprivileged:alpine"
        ],
        ...
    }
]

Terraform removed docker_image.nginx from state, but the image remains in Docker because keep_locally = true. Destroying a Terraform resource does not always mean the provider deletes the underlying local artifact; resource-specific arguments can alter destroy behavior.

List what Terraform still tracks:

bash
terraform state list

With every managed instance destroyed, the command prints nothing — state contains no resource addresses.


terraform destroy options

Skip the confirmation prompt with -auto-approve

-auto-approve runs the destroy plan without the interactive yes prompt. Recreate the lab stack first:

bash
terraform apply -auto-approve

Use non-interactive destroy only when a human or pipeline has already reviewed the plan:

bash
terraform destroy -auto-approve

Reserve -auto-approve for disposable labs and controlled automation. Production teardown should keep the confirmation step or use a reviewed saved destroy plan workflow.

Pass variables with -var and -var-file

Terraform evaluates configuration during destroy, not only during apply. If your .tf files reference var.environment, Terraform needs those values even when every resource is being deleted.

Create a variable file for repeatable lab runs:

bash
printf 'environment = "lab"\n' > test.tfvars

Pass the same values you used at apply time:

bash
terraform destroy -var='environment=lab' -auto-approve

Or reference the file:

bash
terraform destroy -var-file=test.tfvars -auto-approve

Terraform still evaluates configuration while building a destroy plan, so required input variables may still need values. This is especially important when variables affect provider configuration, module inputs, or resource addressing. Use the same variable sources you normally use for that working directory.

Target specific resources

-target limits destruction to one resource address. Recreate the full stack, then destroy only the volume:

Recreate the full stack before you test targeting:

bash
terraform apply -auto-approve

Destroy only the volume address while the docker_volume block remains in configuration:

bash
terraform destroy -target=docker_volume.data -auto-approve

Terraform warns that targeting is exceptional:

output
Note that the -target option is not suitable for routine use, and is provided
only for exceptional situations such as recovering from errors or mistakes...

After a targeted destroy, terraform state list still shows the container, network, and image — only the volume address is gone. Targeted destruction can leave configuration, state, and live infrastructure out of sync. Use it for recovery, not as the normal way to remove a resource from everyday workflows.


Destroy one resource vs remove it from configuration

These workflows look similar but serve different goals.

Scenario A — targeted destroy: run terraform destroy -target=docker_volume.data while the docker_volume block remains in main.tf. Terraform deletes the remote volume but still expects a volume because the block is present. The next terraform plan may propose recreating it.

Scenario B — remove from configuration: delete the docker_volume block from main.tf while the volume still exists, then run a normal plan:

bash
terraform plan

Sample output (trimmed):

output
# docker_volume.data will be destroyed
  - resource "docker_volume" "data" { ... }

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

Apply that plan with terraform apply to remove only the orphaned volume while keeping the rest of the stack under management.

Approach Configuration block Typical intent
terraform destroy -target=... Still present Exceptional recovery or emergency partial teardown
Remove block + terraform plan / apply Removed Permanent model change — resource no longer belongs in this module

For day-to-day changes, remove the resource from configuration and let a normal plan drive deletion. Reserve -target for the exceptional cases Terraform documents in its warning text.


terraform destroy vs plan and apply destroy modes

Command Executes deletions? Confirmation prompt?
terraform plan -destroy No — preview only No
terraform destroy Yes Yes, unless -auto-approve
terraform apply -destroy Yes Yes, unless -auto-approve

terraform destroy and terraform apply -destroy are equivalent destroy-mode entry points in current Terraform. Pick whichever reads clearer in your runbook; you do not need separate examples for both in everyday use.


State and configuration after destroy

What happens to Terraform state?

After a full destroy, terraform state list returns no addresses — there are no managed instances left. The state file usually still exists on disk with an empty resource list (or metadata only). Terraform does not automatically delete terraform.tfstate just because the last resource was destroyed.

Inspect the file when you want to confirm:

bash
wc -l terraform.tfstate

You should still see a JSON state file, not a missing path error.

Configuration files remain

Destroy removes remote objects, not your source code. After teardown:

bash
ls *.tf .terraform.lock.hcl

Sample output:

output
main.tf
.terraform.lock.hcl

Your .tf files and lock file stay in place so you can edit configuration and run terraform apply again. The .terraform/ directory with provider plugins also remains until you delete it.

Block destroy with prevent_destroy

Add a lifecycle guard when a resource must never be deleted accidentally:

hcl
resource "docker_container" "web" {
  lifecycle {
    prevent_destroy = true
  }

  name  = "tf-destroy-web"
  image = docker_image.nginx.image_id
  ...
}

With the guard in place, a full destroy fails while that resource is still in scope:

bash
terraform destroy

Sample output:

output
Error: Instance cannot be destroyed

Resource docker_container.web has lifecycle.prevent_destroy set, but the plan
calls for this resource to be destroyed.

To deliberately destroy that protected object, disable or remove prevent_destroy while keeping the resource configuration available, then review the resulting plan. Targeting can exclude the protected object from a broader operation, but it does not remove the protection from that object. Full prevent_destroy and related rules belong in Terraform lifecycle.

Recreate or remove the lab

After you finish the exercises, either keep ~/terraform-labs/terraform-destroy/ for another run or remove the directory entirely. If resources were destroyed cleanly, deleting the folder is safe.

Do not manually delete terraform.tfstate as a substitute for destroying resources — that orphans remote objects Terraform no longer tracks while they continue running on the host.

To run the walkthrough again:

bash
terraform apply

Common terraform destroy problems

Symptom Likely cause Fix
Instance cannot be destroyed lifecycle { prevent_destroy = true } on a resource in the destroy plan Disable or remove prevent_destroy while the resource block remains, then review the plan
Error acquiring the state lock Another Terraform process holds the lock Wait for the other run to finish, or see Terraform state locking
Provider authentication or connection error Docker daemon stopped or socket permissions missing Confirm docker ps works on the host
Dependency error during destroy Provider delete order or external dependency Read the provider error; resolve the dependency or use documented recovery steps
Resource already deleted manually State still tracks an address that no longer exists Refresh or remove the stale binding — see terraform state commands
Target warning followed by unexpected recreate plan -target left configuration and state misaligned Follow with a full terraform plan and normal apply, or fix configuration

References


Summary

terraform destroy proposes and executes deletion of every resource instance managed by the current configuration and state.

You provisioned a disposable Docker stack, previewed teardown with terraform plan -destroy, cancelled once at the confirmation prompt, then destroyed everything.

Terraform cleared four resource instances from state:

  • The container, network, and volume disappeared from Docker
  • The nginx image remained locally because keep_locally = true

-auto-approve skips the prompt for labs and controlled automation only. -var and -var-file still matter because Terraform reads configuration during destroy.

Targeted destroy is an exceptional tool — removing a resource block and running a normal plan is the everyday way to drop something from your model.

Destroy clears managed infrastructure, not your .tf files or lock file, and usually leaves an empty state file on disk.

The mistake to avoid:

  • Using -target for routine cleanup
  • Deleting state by hand instead of letting Terraform deprovision resources it manages

Next in the Core Workflow track: deeper apply and lifecycle topics once your preview-and-destroy rhythm is solid.

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)