Terraform Docker Provider: Manage Containers and Images as Code

· 2 min read · Docker & Rancher Desktop Tutorials

Terraform for local containers #

Terraform and OpenTofu 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: plan, apply, destroy, and a state file.

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

Prerequisites #

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:

$ 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 #

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
}
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 #

$ 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:

$ 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 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:

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 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.

Next steps #

Automate Rancher Desktop itself: rdctl, snapshots and deployment profiles.

#Docker #Terraform #OpenTofu #Rancher Desktop