| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1kreuzwerker/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 apply workflow — interactive approval, automatic plan mode, saved plan files, -auto-approve, -var and -var-file, in-place updates, -replace, -parallelism, partial failure behavior, resource verification with Docker, and comparisons with plan and destroy. Does not cover full plan flag reference, destroy tutorials, CI/CD pipelines, variable precedence, or provider authentication setup. |
| Related guides | terraform plan command terraform init command terraform validate command Terraform lab environment on Ubuntu Terraform Associate certification course |
terraform apply is where configuration becomes real infrastructure. After you declare resources in .tf files and initialize the working directory, apply executes the create, update, and destroy operations Terraform planned against your providers.
This walkthrough uses the Docker provider on Ubuntu so you can confirm every change with docker ps and docker inspect, not only Terraform’s success messages. Work in ~/terraform-labs/terraform-apply/ throughout.
terraform plan flags, see the terraform plan command guide.
How terraform apply works
terraform apply executes the operations needed to make managed infrastructure match the selected Terraform plan.
- It reads configuration and state
- It calls provider APIs
- It writes results back to state
- Apply does not roll back successful operations automatically when a later step fails
Automatic plan mode
The default workflow is a single command:
terraform apply
→ build plan from configuration + state
→ show proposed actions
→ wait for approval (unless skipped)
→ execute operationsThis is the mode you use for day-to-day development when you want Terraform to plan from the current directory and pause for confirmation.
Saved plan mode
Reviewed and automated workflows often split planning from execution:
terraform plan -out=tfplan
terraform apply tfplan
→ execute the saved plan only (no replan, no approval prompt)Saved plan mode fits handoffs between teams, pipeline gates, and any case where you must apply exactly what was reviewed earlier.
Run and verify terraform apply
The sections below walk through one Docker container lab from first create through independent verification and a no-change re-apply.
Prepare the Docker lab
Create the working directory:
mkdir -p ~/terraform-labs/terraform-apply && cd ~/terraform-labs/terraform-applyWrite the root module. This lab uses the ghcr.io/nginx/nginx-unprivileged:alpine image and container name tf-apply-web on host port 8082:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
provider "docker" {}
variable "environment" {
type = string
default = "dev"
}
resource "docker_image" "nginx" {
name = "ghcr.io/nginx/nginx-unprivileged:alpine"
}
resource "docker_container" "web" {
name = "tf-apply-web"
image = docker_image.nginx.image_id
restart = "unless-stopped"
labels {
label = "environment"
value = var.environment
}
ports {
internal = 8080
external = 8082
}
}
EOFInitialize providers:
terraform init -input=falseTerraform has been successfully initialized!Review and approve the plan
Run apply without -auto-approve so Terraform shows the execution plan and waits for your decision:
terraform applyRead the proposed + create actions, then at the prompt type yes and press Enter. Terraform accepts only the literal word yes:
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 15s
docker_container.web: Creating...
docker_container.web: Creation complete after 1s
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.First image pull can take longer than later runs depending on cache and network. Any answer other than yes cancels the apply without changes.
Verify the container outside Terraform
Terraform’s Apply complete! line confirms provider API calls succeeded. Confirm the container exists on the host:
docker ps --filter name=tf-apply-webCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
444c18abcf88 334d92979f15 "/docker-entrypoint.…" 1 second ago Up 1 second 0.0.0.0:8082->8080/tcp tf-apply-webRead the label Terraform set from var.environment:
docker inspect tf-apply-web --format '{{index .Config.Labels "environment"}}'devList managed resource addresses in state:
terraform state listdocker_container.web
docker_image.nginxState maps configuration addresses like docker_container.web to real container and image IDs. When verification disagrees with state, run terraform plan before you apply again.
What happens when nothing changed
Run apply again without editing main.tf:
terraform applyTerraform refreshes state, compares configuration to live infrastructure, and reports that nothing needs to change — there is no approval prompt because the plan is empty:
No changes. Your infrastructure matches the configuration.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.That idempotent result is normal — apply converges infrastructure toward configuration, not toward “always do something.”
Apply plans and common options
The subsections below reuse the same working directory. Later examples overwrite main.tf with heredocs where the configuration must change.
Apply a saved plan
Saved plans separate planning from execution. Change restart to no in main.tf:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
provider "docker" {}
variable "environment" {
type = string
default = "dev"
}
resource "docker_image" "nginx" {
name = "ghcr.io/nginx/nginx-unprivileged:alpine"
}
resource "docker_container" "web" {
name = "tf-apply-web"
image = docker_image.nginx.image_id
restart = "no"
labels {
label = "environment"
value = var.environment
}
ports {
internal = 8080
external = 8082
}
}
EOFWrite the plan to a file instead of applying immediately:
terraform plan -out=tfplan# docker_container.web will be updated in-place
~ resource "docker_container" "web" {
~ restart = "unless-stopped" -> "no"
}
Plan: 0 to add, 1 to change, 0 to destroy.
Saved the plan to: tfplanReview the artifact, then apply exactly that plan. terraform apply tfplan does not replan and does not ask for approval:
terraform apply tfplandocker_container.web: Modifying...
docker_container.web: Modifications complete after 0s
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.Treat saved plan files as potentially sensitive artifacts. They contain the full planned changes and can store sensitive values in cleartext.
Skip approval with -auto-approve
-auto-approve skips the interactive prompt and applies the generated plan immediately.
Change restart back to unless-stopped in main.tf, then run:
terraform apply -auto-approve# docker_container.web will be updated in-place
~ resource "docker_container" "web" {
~ restart = "no" -> "unless-stopped"
}
Plan: 0 to add, 1 to change, 0 to destroy.
docker_container.web: Modifying...
docker_container.web: Modifications complete after 0s
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.Use -auto-approve in disposable labs and controlled automation. Prefer interactive apply or a saved plan workflow for production changes you have not reviewed.
Pass variables with -var and -var-file
Pass values at apply time without editing main.tf.
Override environment for one run:
terraform apply -var='environment=staging' -auto-approvePlan: 1 to add, 0 to change, 1 to destroy.
docker_container.web: Destroying...
docker_container.web: Destruction complete after 0s
docker_container.web: Creating...
docker_container.web: Creation complete after 1s
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.Create a variable file for repeatable values:
printf 'environment = "qa"\n' > test.tfvarsLoad it during apply:
terraform apply -var-file=test.tfvars -auto-approvePlan: 1 to add, 0 to change, 1 to destroy.
...
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.Confirm the label on the running container:
docker inspect tf-apply-web --format '{{index .Config.Labels "environment"}}'qaVariable precedence between CLI flags, files, and defaults is out of scope here — this section shows only how to pass values at apply time.
Force replacement with -replace
-replace requests recreation of a specific resource even when configuration and state already agree.
Align main.tf with the live container after the qa apply — set default = "qa":
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
provider "docker" {}
variable "environment" {
type = string
default = "qa"
}
resource "docker_image" "nginx" {
name = "ghcr.io/nginx/nginx-unprivileged:alpine"
}
resource "docker_container" "web" {
name = "tf-apply-web"
image = docker_image.nginx.image_id
restart = "unless-stopped"
labels {
label = "environment"
value = var.environment
}
ports {
internal = 8080
external = 8082
}
}
EOFA normal plan shows nothing to do:
terraform planNo changes. Your infrastructure matches the configuration.Request replacement explicitly on apply:
terraform apply -replace=docker_container.web -auto-approve# docker_container.web will be replaced, as requested
-/+ resource "docker_container" "web" {
Plan: 1 to add, 0 to change, 1 to destroy.
docker_container.web: Destroying...
docker_container.web: Destruction complete after 1s
docker_container.web: Creating...
docker_container.web: Creation complete after 1s
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.Nothing needed changing in configuration, but -replace forced recreation anyway. Deleting a container manually outside Terraform causes drift — Terraform will plan recreation on the next run, but that is not the same as a requested replace.
Limit concurrency with -parallelism
-parallelism=n limits how many resource operations Terraform runs concurrently during apply. The default is 10. It is mainly useful when API or provider rate limits require lower concurrency — raising the value does not always speed up applies because dependency ordering and provider latency still apply.
How terraform apply handles changes and failures
In-place update vs replacement
Plan symbols tell you how apply will change each resource:
| Change type | Plan symbol | Example in this lab |
|---|---|---|
| Restart policy change | ~ update in-place |
restart = "unless-stopped" → "no" via saved plan |
| Label value change | -/+ replace |
terraform apply -var='environment=staging' |
Explicit -replace |
-/+ replace (as requested) |
terraform apply -replace=docker_container.web when plan was empty |
If the plan shows ~, expect the same resource address in state afterward. If it shows -/+, Terraform destroys the old object and creates a new one.
Partial apply failures
Apply is not transactional. Terraform may complete some operations, update state for those successes, and stop when a later operation errors.
Add an invalid bind mount to reproduce a safe failure:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
provider "docker" {}
variable "environment" {
type = string
default = "qa"
}
resource "docker_image" "nginx" {
name = "ghcr.io/nginx/nginx-unprivileged:alpine"
}
resource "docker_container" "web" {
name = "tf-apply-web"
image = docker_image.nginx.image_id
restart = "unless-stopped"
labels {
label = "environment"
value = var.environment
}
ports {
internal = 8080
external = 8082
}
mounts {
target = "/data"
source = "/nonexistent/terraform-apply-lab-bind-path"
type = "bind"
}
}
EOFRun apply:
terraform apply -auto-approvedocker_container.web: Destroying...
docker_container.web: Destruction complete after 0s
docker_container.web: Creating...
╷
│ Error: Unable to create container: Error response from daemon: invalid mount config for type "bind": bind source path does not exist: /nonexistent/terraform-apply-lab-bind-path
│
│ with docker_container.web,
│ on main.tf line 23, in resource "docker_container" "web":
╵The old container was destroyed before the replacement failed. Check state:
terraform state listdocker_image.nginxdocker_container.web is gone from state and the container no longer appears in docker ps. Restore the working configuration without the mounts block:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
provider "docker" {}
variable "environment" {
type = string
default = "qa"
}
resource "docker_image" "nginx" {
name = "ghcr.io/nginx/nginx-unprivileged:alpine"
}
resource "docker_container" "web" {
name = "tf-apply-web"
image = docker_image.nginx.image_id
restart = "unless-stopped"
labels {
label = "environment"
value = var.environment
}
ports {
internal = 8080
external = 8082
}
}
EOFPlan shows Terraform needs to recreate the missing container:
terraform planPlan: 1 to add, 0 to change, 0 to destroy.Apply the fix:
terraform apply -auto-approvedocker_container.web: Creating...
docker_container.web: Creation complete after 1s
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.The container is back in state and running on the host before you continue to cleanup.
Interrupting terraform apply
If you press Ctrl+C during apply, Terraform attempts to stop gracefully. Behavior depends on which operation was in progress and how the provider handles cancellation.
Do not assume interrupt rolls back completed steps. Some resources may already exist, and state may reflect partial progress. Run terraform plan after an interrupt to see what still diverges from configuration, then apply again to converge.
terraform apply vs plan vs destroy
| Command | Purpose | Changes infrastructure |
|---|---|---|
terraform plan |
Preview proposed changes | No (normal speculative plan) |
terraform apply |
Execute a plan | Yes |
terraform destroy |
Tear down managed resources | Yes (destroy actions) |
terraform plan answers what would change. terraform apply performs the change. terraform destroy is a specialized apply path that removes managed resources — see the terraform destroy lesson for the full teardown workflow.
You normally do not need terraform validate immediately before every plan or apply when you are already in a full workflow; plan includes an implied validation check. Use terraform validate as a fast standalone or CI gate.
Common terraform apply errors
| Symptom | Likely cause | Next step |
|---|---|---|
| Provider API or daemon error | Invalid argument, missing host path, quota limit | Read the provider error; fix configuration or environment; plan again |
| Insufficient permissions | IAM, RBAC, or Docker socket access | Fix credentials or group membership; retry |
| Error acquiring state lock | Another process holds the backend lock | Wait or follow your backend’s lock guidance |
| Resource already exists | Name conflict outside Terraform state | Import, rename, or remove the conflicting object |
| Timeout | Slow API or network | Retry; check provider status; adjust timeouts if supported |
| Dependency failure | Parent resource failed earlier in the graph | Fix the root error; plan shows remaining work |
| Invalid variable input | Wrong type or missing required variable | Fix -var, tfvars, or variable definitions |
The bind-mount failure above is a provider API error: destroy succeeded, create failed, and state no longer listed the container.
Cleanup
When you finish the lab, tear down managed resources:
terraform destroy -auto-approvePlan: 0 to add, 0 to change, 2 to destroy.
docker_container.web: Destroying...
docker_container.web: Destruction complete after 1s
docker_image.nginx: Destroying...
docker_image.nginx: Destruction complete after 0s
Destroy complete! Resources: 2 destroyed.Confirm Docker no longer shows the managed container:
docker ps -a --filter name=tf-apply-webCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESAn empty table means the lab container is gone. State should be empty after a successful destroy.
References
- terraform apply command — HashiCorp Terraform CLI documentation
- terraform plan command — planning and saved plan files
- Apply Terraform Configuration — HashiCorp tutorial
- Docker provider documentation
Summary
terraform apply turns Terraform's execution plan into real infrastructure changes.
You ran interactive apply with the literal yes prompt, verified the Docker container with docker ps and docker inspect, matched live objects to terraform state list, and saw a no-change re-apply when configuration already matched reality.
Saved plans with terraform plan -out=tfplan and terraform apply tfplan separate review from execution.
Flags such as -auto-approve, -var, -var-file, and -replace cover the cases you reach for after the basics — including -replace on a configuration that already showed no changes.
The bind-mount failure demonstrated partial apply:
- Destroy completed
- Create failed
- State no longer listed the container
Apply is not an automatic rollback. After any failure or interrupt, plan again, fix configuration or credentials, and re-apply to converge. Finish labs with terraform destroy so containers do not keep running on the host.

