# Terraform Docker Provider: Manage Containers and Images as Code

> Use the Terraform and OpenTofu Docker provider (kreuzwerker/docker) with Rancher Desktop to manage images, networks, volumes and containers declaratively.

- Source: https://www.itwonderlab.com/terraform-docker-provider/
- Published: 2026-10-06
- Updated: 2026-10-06
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## Terraform for local containers

[Terraform and OpenTofu](https://www.itwonderlab.com/terraform-provider/) are known for cloud infrastructure, but a **provider** exists for almost everything, including Docker. The `kreuzwerker/docker` provider talks to the Docker API and manages images, containers, networks and volumes with the same workflow you use for [AWS](https://www.itwonderlab.com/aws-terraform-tutorial-terraform-basics/): `plan`, `apply`, `destroy`, and a [state file](https://www.itwonderlab.com/terraform-state/).

It is useful for repeatable development environments, for integration test fixtures, and to learn Terraform without a cloud account.

## Prerequisites

- [Rancher Desktop](https://www.itwonderlab.com/rancher-desktop-install/) with the **dockerd (moby)** engine. The provider needs the Docker API socket, which containerd does not provide. See [containerd vs dockerd](https://www.itwonderlab.com/rancher-desktop-containerd-vs-dockerd/).
- [Terraform](https://www.itwonderlab.com/aws-terraform-tutorial-terraform-basics/) or [OpenTofu](https://www.itwonderlab.com/how-to-install-opentofu/) installed.

## Find the Docker host

The provider needs the address of the Docker socket. Ask the Docker context that Rancher Desktop created instead of guessing the path:

```shell
$ docker context inspect rancher-desktop --format '{{.Endpoints.docker.Host}}'
unix:///home/you/.rd/docker.sock
```

On Windows the host is a named pipe such as `npipe:////./pipe/docker_engine`. Use the value you get.

## The configuration

```hcl title="versions.tf"
terraform {
  required_version = ">= 1.6"

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

variable "docker_host" {
  type        = string
  description = "Docker API endpoint, see: docker context inspect rancher-desktop"
}

provider "docker" {
  host = var.docker_host
}
```

```hcl title="main.tf"
resource "docker_network" "app" {
  name = "app-net"
}

resource "docker_volume" "db" {
  name = "app-db-data"
}

resource "docker_image" "postgres" {
  name         = "postgres:17"
  keep_locally = true            # do not delete the image on destroy
}

resource "docker_image" "nginx" {
  name         = "nginx:1.27"
  keep_locally = true
}

resource "docker_container" "db" {
  name  = "app-db"
  image = docker_image.postgres.image_id

  env = [
    "POSTGRES_USER=app",
    "POSTGRES_PASSWORD=${var.db_password}",
    "POSTGRES_DB=app",
  ]

  networks_advanced {
    name    = docker_network.app.name
    aliases = ["db"]
  }

  volumes {
    volume_name    = docker_volume.db.name
    container_path = "/var/lib/postgresql/data"
  }

  restart = "unless-stopped"
}

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

  ports {
    internal = 80
    external = 8080
    ip       = "127.0.0.1"
  }

  networks_advanced {
    name = docker_network.app.name
  }

  depends_on = [docker_container.db]
}

variable "db_password" {
  type      = string
  sensitive = true
}
```

Each resource maps to a command you already know: `docker_network` to `docker network create`, `docker_volume` to `docker volume create`, `docker_image` to `docker pull`, and `docker_container` to `docker run`. References such as `docker_image.nginx.image_id` create the dependencies, so Terraform pulls the image before it creates the container.

## Run it

```shell
$ export TF_VAR_docker_host="$(docker context inspect rancher-desktop --format '{{.Endpoints.docker.Host}}')"
$ export TF_VAR_db_password="change-me"
$ terraform init
$ terraform plan
$ terraform apply
$ curl -I http://localhost:8080
HTTP/1.1 200 OK
$ docker ps
```

Because Terraform tracks the state, a change to the configuration produces a plan with only the difference. Change the port to 8081 and apply: Terraform destroys and recreates the web container because the port mapping of a container cannot be modified in place. Remove everything:

```shell
$ terraform destroy
```

The named volume is removed as well, so the data of the database is gone. To keep a volume, remove it from the state first or protect it with `lifecycle { prevent_destroy = true }`, as explained in the [lifecycle meta-argument](https://www.itwonderlab.com/terraform-lifecycle-meta-argument/) tutorial.

## Build images with Terraform

`docker_image` can build from a Dockerfile, which is handy for a tiny fixture and to rebuild when files change:

```hcl
resource "docker_image" "app" {
  name = "myapp:dev"

  build {
    context = "${path.module}/app"
  }

  triggers = {
    dir_sha1 = sha1(join("", [for f in fileset("${path.module}/app", "**") : filesha1("${path.module}/app/${f}")]))
  }
}
```

For anything beyond experiments, build in your CI pipeline with Docker or BuildKit and let Terraform only run the resulting image.

## When to use it

- **Good fit**: local environments, test infrastructure that must be created and destroyed from code, and demos.
- **Poor fit**: day-to-day application deployment. Use [Docker Compose](https://www.itwonderlab.com/docker-compose-tutorial/) for developers and Kubernetes or ECS for production.

The same provider works against remote Docker hosts (`ssh://user@host`) and against any compatible engine, so the code you write here is also a good start for managing Docker on a virtual machine you created with Terraform in [AWS](https://www.itwonderlab.com/aws-terraform-tutorial-aws-ec2/).

## Next steps

Automate Rancher Desktop itself: [rdctl, snapshots and deployment profiles](https://www.itwonderlab.com/rancher-desktop-rdctl-automation/).
