Terraform Docker Provider: Manage Containers and Images as Code
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 #
- Rancher Desktop with the dockerd (moby) engine. The provider needs the Docker API socket, which containerd does not provide. See containerd vs dockerd.
- Terraform or 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:
$ 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 #
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
}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.