| 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 dynamic block syntax, for_each, iterator, content, conditional nested blocks, docker_container labels and ports lab, one nested dynamic example, comparison with resource for_each and for expressions, readability guidance, limitations, and common errors. Does not cover count or for_each on resources in depth, full module design, or cloud-only schemas. |
| Related guides | Terraform count vs for_each Terraform expressions Terraform resources Terraform variables Terraform Associate certification course |
Some provider schemas expect the same nested block repeated many times — container labels, security group rules, listener ports. Copy-pasting ten identical labels blocks works until the list grows or comes from a variable.
A dynamic block generates those repetitions from a for_each collection. The result is still one docker_container (or one aws_security_group, or one azurerm_* resource) with nested blocks expanded before Terraform talks to the provider.
This guide walks through real docker_container examples on Terraform 1.15.8 — static blocks first, then dynamic refactors you can plan, apply, and verify with docker inspect.
~/terraform-labs/terraform-dynamic-block/ on the Terraform lab environment on Ubuntu. Run terraform init in each subdirectory before plan or apply. Examples use the Docker provider and docker_container nested labels, ports, and mounts blocks validated against provider schema 3.9.0. The lab image is nginx:alpine, which listens on container port 80 by default. If Docker Hub rate-limits pulls, use the official mirror public.ecr.aws/docker/library/nginx:alpine in docker_image.nginx.
What is a Terraform dynamic block?
A dynamic block is language sugar for repeatable nested blocks. Terraform evaluates the for_each collection, then materializes one nested block per element inside the parent block.
resource for_each → multiple resource instances (docker_container.web["a"], docker_container.web["b"])
dynamic block → multiple nested blocks inside one instance (many labels blocks inside docker_container.web)Dynamic blocks do not create arbitrary resource blocks. You cannot write dynamic "aws_instance" and spawn instances from a loop — that remains the job of count and for_each on resources.
They apply wherever the parent schema allows nested blocks: resource, data, provider, and provisioner configurations that declare repeatable block types.
Dynamic block syntax
Every dynamic block shares the same shape:
dynamic "<NESTED_BLOCK_TYPE>" {
for_each = <collection>
iterator = <name> # optional; defaults to the block type name
labels = [...] # only when the nested block type uses block labels
content {
# arguments for one nested block instance
}
}| Piece | Role |
|---|---|
"<NESTED_BLOCK_TYPE>" |
Must match a nested block type in the provider schema (labels, ports, mounts, …) |
for_each |
Collection or structural value with one element per nested block to generate |
iterator |
Optional alias for the current element; default is the block type name (labels, ports, …) |
labels |
Required only for nested block types that use block labels in the schema |
content |
Body of one generated nested block; references use iterator.key and iterator.value |
Inside content, the default iterator exposes .key and .value. For a collection of objects, .value is the current object. For lists and tuples, .key is the element index; for maps it is the map key. For sets, key is identical to value, so HashiCorp recommends not relying on it.
Unlike resource-level for_each, a dynamic block is not creating independently addressed resource instances. Its for_each can therefore iterate over collection or structural values appropriate to the nested-block generation — a broader set than the map-or-set-of-strings rule that governs resource for_each.
count is not valid on a dynamic block. To make a nested block optional, drive for_each with a conditional collection instead.
Basic dynamic block example
The main lab at ~/terraform-labs/terraform-dynamic-block/main/ starts with repeated static labels blocks, then refactors them to dynamic "labels".
Static version (commented in the lab file for comparison):
labels {
label = "managed_by"
value = "terraform"
}
labels {
label = "lab"
value = "dynamic-block"
}The same labels driven by a list variable:
dynamic "labels" {
for_each = var.container_labels
content {
label = labels.value.label
value = labels.value.value
}
}var.container_labels defaults to two objects (managed_by, lab). Terraform generates one labels nested block per list element.
Initialize the lab directory:
cd ~/terraform-labs/terraform-dynamic-block/main && terraform initApply the configuration so the dynamic labels and conditional port publish to a real container:
cd ~/terraform-labs/terraform-dynamic-block/main && terraform apply -auto-approve -input=false -no-colorThe apply creates a network, pulls nginx:alpine, and starts tf-dynamic-block-main with four dynamic label blocks and one ports block.
Confirm Docker published the host port mapping:
docker ps --filter name=tf-dynamic-block-main --format '{{.Names}} {{.Ports}}'tf-dynamic-block-main 0.0.0.0:18080->80/tcpThe mapping points host port 18080 at container port 80, where nginx:alpine listens by default. Verify the dynamic ports block actually forwards traffic:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080200An HTTP 200 confirms the generated nested ports block works end to end, not only that Docker recorded a mapping.
Inspect the Terraform-managed labels:
docker inspect tf-dynamic-block-main --format '{{json .Config.Labels}}'{"env":"lab","lab":"dynamic-block","managed_by":"terraform","project":"terraform-dynamic-block"}Four keys appear because the lab runs two dynamic "labels" blocks — one over the list variable and one over a map — which is equivalent to writing four static labels blocks.
Iterator and conditional dynamic blocks
dynamic with for_each on a map
The second dynamic "labels" block in the lab iterates var.tags with a custom iterator:
dynamic "labels" {
for_each = var.tags
iterator = tag
content {
label = tag.key
value = tag.value
}
}When iterator is omitted, Terraform names the iterator after the block type. With iterator = tag, references inside content must use tag.key and tag.value, not labels.key.
Conditional nested blocks
To include a ports block only when publishing is enabled:
dynamic "ports" {
for_each = var.expose_port ? [{ internal = 80, external = var.host_port }] : []
content {
internal = ports.value.internal
external = ports.value.external
}
}When var.expose_port is false, for_each is an empty list and Terraform generates zero ports blocks. This replaces patterns like count = var.enabled ? 1 : 0 that work on resources but are unavailable inside dynamic.
Set expose_port to false in a tfvars file or variable override and run plan — the ports nested block disappears from the diff while the container resource remains.
Nested dynamic blocks
You can nest dynamic inside content when the schema has nested block types inside nested block types. Keep depth shallow; one practical example is enough for most modules.
The nested/ lab mounts a volume with labels on volume_options:
dynamic "mounts" {
for_each = var.mount_specs
content {
type = "volume"
source = docker_volume.data.name
target = mounts.value.target
dynamic "volume_options" {
for_each = length(mounts.value.labels) > 0 ? [mounts.value.labels] : []
content {
dynamic "labels" {
for_each = volume_options.value
iterator = vol_label
content {
label = vol_label.key
value = vol_label.value
}
}
}
}
}
}Each iterator name (mounts, volume_options, vol_label) scopes references in its content block. Reusing the wrong name is a common source of validate errors.
Initialize and apply the nested example:
cd ~/terraform-labs/terraform-dynamic-block/nested && terraform initApply so Terraform creates the volume mount with nested dynamic labels:
cd ~/terraform-labs/terraform-dynamic-block/nested && terraform apply -auto-approve -input=false -no-colorPlan and apply succeed with three resources (docker_volume, docker_image, docker_container). The plan shows mounts with nested volume_options and labels blocks generated from var.mount_specs.
Dynamic blocks vs resource for_each and for expressions
These three constructs look similar but solve different problems:
| Mechanism | What it produces | Example address or result |
|---|---|---|
Resource for_each |
Multiple resource instances | docker_container.web["api"] |
dynamic block |
Repeated nested blocks in one instance | Four labels blocks inside docker_container.web |
for expression |
A list, map, or object value | [for x in var.names : upper(x)] |
resource for_each → N separate resources
dynamic → N nested blocks inside 1 resource
for expression → 1 value (collection or structure)Use resource for_each when each collection element should be its own managed object with a distinct state address. Use dynamic when the provider expects repeated nested configuration inside a single object. Use a for expression when you need to transform data, not emit nested blocks.
Full meta-argument workflows live in Terraform count vs for_each. Terraform expressions covers for expression syntax in depth.
When not to use dynamic blocks and limitations
Dynamic blocks are not automatically clearer than static blocks. If a resource needs one or two nested blocks, write them explicitly. Reach for dynamic when repetition is real and driven by variables.
Signs you may have gone too far:
- Nearly every nested block in the resource is
dynamic, and the file is harder to read than a flat list - You are generating nested blocks to work around a missing resource
for_each— split into multiple resources instead - Iterator names are reused carelessly across three or more nesting levels
Schema and meta-argument limits
Dynamic blocks can only generate nested block types the provider schema defines for that parent block. They cannot invent block types (metadata on docker_container fails validate) and cannot generate resource-level meta-argument blocks such as lifecycle or provider.
Terraform evaluates a dynamic block and generates ordinary nested blocks before passing the resulting resource configuration to the provider. The provider still sees static nested blocks — dynamic is Terraform language behavior, similar to a for expression that produces blocks rather than a single value.
for_each on a dynamic block must be a collection or structural value Terraform can iterate — not a bare string or number. That acceptance is broader than resource-level for_each, which is limited to maps and sets of strings for instance addressing.
Functions such as file() inside a dynamic content block follow the same rules as static nested blocks. They do not join the dependency graph differently.
Common Terraform dynamic block errors
The errors/ tree under ~/terraform-labs/terraform-dynamic-block/ reproduces failures you are likely to see in review.
| Symptom | Likely cause | Fix |
|---|---|---|
Invalid dynamic for_each value |
for_each is not iterable |
Pass a collection or structural value, not a bare string or number |
Reference to undeclared resource on labels.key |
Custom iterator set but default name used in content |
Reference tag.key when iterator = tag |
Unsupported block type |
Nested block name not in schema | Check provider docs; use only declared block types |
| Plan shows zero nested blocks | for_each is empty |
Confirm variable values; conditional may be false |
| Duplicate nested block keys | Set-style nesting deduplicates | Ensure label/port keys are unique per schema rules |
Invalid for_each type — errors/invalid-for-each/:
cd ~/terraform-labs/terraform-dynamic-block/errors/invalid-for-each && terraform init && terraform validate -no-colorError: Invalid dynamic for_each value
on main.tf line 10, in resource "docker_container" "web":
10: for_each = "not-a-collection"
Cannot use a string value in for_each. An iterable collection is required.Wrong iterator reference — errors/wrong-iterator/:
cd ~/terraform-labs/terraform-dynamic-block/errors/wrong-iterator && terraform init && terraform validate -no-colorError: Reference to undeclared resource
on main.tf line 14, in resource "docker_container" "web":
14: label = labels.key
A managed resource "labels" "key" has not been declared in the root module.Unsupported nested block — errors/unsupported-block/:
cd ~/terraform-labs/terraform-dynamic-block/errors/unsupported-block && terraform init && terraform validate -no-colorError: Unsupported block type
on main.tf line 9, in resource "docker_container" "web":
9: dynamic "metadata" {
Blocks of type "metadata" are not expected here.Official References
- Dynamic Blocks — Terraform configuration language
- for_each meta-argument — Terraform configuration language
- Expressions — for — Terraform configuration language
- docker_container — kreuzwerker/docker provider documentation
Summary
You started from repeated static nested blocks on docker_container, then refactored them to dynamic "labels" and dynamic "ports" driven by variables. The for_each collection controls how many nested blocks Terraform generates; the optional iterator argument renames the loop variable inside content.
Conditional nested blocks use for_each = condition ? [one_element] : [] because count is not available on dynamic. That pattern publishes container port 80 on host port 18080 when var.expose_port is true — matching the default nginx:alpine listener. Nested dynamic blocks work when the schema supports deeper block types — the lab mounts example chains mounts, volume_options, and labels with distinct iterator names.
Dynamic blocks repeat nested configuration inside one resource. Resource for_each creates multiple instances; for expressions transform values. Do not use dynamic to spawn resources or to replace readable static configuration. When labels or rules are few and fixed, explicit blocks are often the better choice. Next, compare instance repetition in Terraform count vs for_each or wire dynamic labels into Terraform variables from tfvars.

