Terraform with Docker Provider

The Terraform Docker provider enables managing Docker containers, images, networks, and volumes using Terraform's declarative infrastructure-as-code approach. This integration allows you to define containerized infrastructure in code, enabling version control, repeatable deployments, and multi-environment consistency. This guide covers Docker provider configuration, container management, image handling, networking, volumes, Docker Compose comparison, and practical examples.

Docker Provider Overview

The Terraform Docker provider enables infrastructure-as-code management of Docker resources. Instead of manually running docker commands, you define desired container infrastructure in Terraform configuration, enabling version control, collaboration, and automated deployment.

Key advantages:

  • Infrastructure as Code: Manage containers like other infrastructure
  • Version Control: Track container configuration changes in git
  • Repeatability: Spin up identical environments reliably
  • Multi-Environment: Use same configuration across dev/staging/prod
  • Integration: Combine with other Terraform resources
  • Documentation: Terraform becomes infrastructure documentation

Docker provider architecture:

┌──────────────────────┐
│ Terraform Config     │
│ (docker resources)   │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│ Docker Provider      │
│ (Terraform Plugin)   │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│ Docker API           │
│ (Local/Remote)       │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│ Docker Engine        │
│ (Create/Manage)      │
└──────────────────────┘

Provider Configuration

Configure Terraform to use the Docker provider.

Basic provider configuration:

# versions.tf
terraform {
  required_version = ">= 1.0"

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

provider "docker" {
  host = "unix:///var/run/docker.sock"
}

Remote Docker configuration:

provider "docker" {
  # Connect to remote Docker daemon
  host = "tcp://docker-host.example.com:2375"
  
  # Enable TLS for secure connection
  # host = "tcp://docker-host.example.com:2376"
  # ca_data   = file("${path.module}/ca.pem")
  # cert_data = file("${path.module}/cert.pem")
  # key_data  = file("${path.module}/key.pem")
}

Docker registry configuration:

provider "docker" {
  host = "unix:///var/run/docker.sock"

  # Configure registries
  registry_auth {
    address             = "ghcr.io"
    username            = var.ghcr_username
    password            = var.ghcr_password
  }

  registry_auth {
    address  = "docker.io"
    username = var.dockerhub_username
    password = var.dockerhub_password
  }
}

Variables for credentials:

# variables.tf
variable "docker_host" {
  type        = string
  description = "Docker daemon socket/host"
  default     = "unix:///var/run/docker.sock"
}

variable "ghcr_username" {
  type        = string
  description = "GitHub Container Registry username"
  sensitive   = true
}

variable "ghcr_password" {
  type        = string
  description = "GitHub Container Registry password"
  sensitive   = true
}

Managing Docker Images

Build and manage Docker images with Terraform.

Pull existing image:

# main.tf
# Pull image from Docker Hub
resource "docker_image" "ubuntu" {
  name         = "ubuntu:22.04"
  keep_locally = true  # Keep image after destroy
}

# Pull from private registry
resource "docker_image" "private" {
  name         = "ghcr.io/myorg/myapp:latest"
  keep_locally = true
  
  pull_triggers = [
    var.image_tag  # Repull on tag change
  ]
}

Build image from Dockerfile:

# Build image
resource "docker_image" "myapp" {
  name = "myapp:1.0"

  build {
    context      = "${path.module}/app"
    dockerfile   = "Dockerfile"
    force_remove = true

    build_arg = {
      ENVIRONMENT = var.environment
      VERSION     = var.app_version
    }

    label = {
      Name        = "myapp"
      Environment = var.environment
    }
  }
}

# Use built image
resource "docker_container" "app" {
  image = docker_image.myapp.image_id
  name  = "myapp-container"
}

Build with registry push:

# Build and push to registry
resource "docker_image" "registry" {
  name = "ghcr.io/myorg/myapp:${var.version}"

  build {
    context    = "${path.module}/app"
    dockerfile = "Dockerfile"
  }
  
  # Push after build
  force_remove = true
  
  # Push to registry
  registry_digest = docker_registry_image.registry_image.digest
}

resource "docker_registry_image" "registry_image" {
  name = docker_image.registry.name
}

Image outputs:

output "image_id" {
  value       = docker_image.myapp.image_id
  description = "Image ID"
}

output "image_sha" {
  value       = docker_image.myapp.sha256_digest
  description = "Image SHA256 digest"
}

output "image_repo_digest" {
  value       = docker_image.myapp.repo_digest
  description = "Repository digest"
}

Managing Containers

Create and manage Docker containers.

Basic container:

resource "docker_container" "web" {
  image = "nginx:latest"
  name  = "web-server"

  ports {
    internal = 80
    external = 8080
  }

  env = [
    "NGINX_HOST=example.com",
    "NGINX_PORT=80"
  ]

  restart_policy = "on-failure"
}

Advanced container configuration:

resource "docker_container" "app" {
  image = docker_image.myapp.image_id
  name  = "myapp-${var.environment}"

  # Port mapping
  ports {
    internal = 3000
    external = 3000
  }

  ports {
    internal = 3001
    external = 3001
    protocol = "tcp"
  }

  # Port ranges
  ports {
    internal = 5000
    external = 5001
    ip       = "127.0.0.1"
  }

  # Environment variables
  env = [
    "APP_ENV=${var.environment}",
    "APP_VERSION=${var.app_version}",
    "DATABASE_URL=${var.database_url}",
  ]

  # Mounts
  mounts {
    type        = "bind"
    source      = "/host/data"
    target      = "/container/data"
    read_only   = false
  }

  mounts {
    type   = "volume"
    source = docker_volume.app_data.name
    target = "/app/data"
  }

  # Volume management
  volumes {
    container_path = "/data"
    host_path      = "/host/data"
    read_only      = false
  }

  # Resource limits
  memory     = 512  # MB
  memory_swap = -1  # Unlimited swap
  cpus        = 1.0

  # Logging
  log_driver = "json-file"
  log_opts = {
    "max-size" = "10m"
    "max-file" = "3"
  }

  # Restart policy
  restart_policy = "on-failure"
  max_retries    = 5

  # Privileged and capabilities
  privileged = false
  capabilities {
    add  = ["NET_ADMIN"]
    drop = ["ALL"]
  }

  # Health check
  healthchecks {
    test         = ["CMD", "curl", "-f", "http://localhost:3000/health"]
    interval     = "30s"
    timeout      = "5s"
    start_period = "5s"
    retries      = 3
  }

  # Depends on other containers
  depends_on = [
    docker_container.database
  ]

  # DNS and hosts
  dns     = ["8.8.8.8", "8.8.4.4"]
  dns_search = ["example.com"]
  hostname = "app-server"

  # User and groups
  user = "appuser"

  # Working directory
  working_dir = "/app"

  # Command and entrypoint
  command     = ["npm", "start"]
  entrypoint  = ["/entrypoint.sh"]

  # Labels
  labels = {
    "com.example.app"  = "myapp"
    "environment"      = var.environment
    "managed_by"       = "terraform"
  }
}

Docker Networks

Create and manage Docker networks for container communication.

Create network:

resource "docker_network" "private" {
  name           = "app-network"
  driver         = "bridge"
  check_duplicate = true
  
  ipam_config {
    subnet = "172.20.0.0/16"
  }

  labels = {
    "network_type" = "private"
  }
}

# Connect containers to network
resource "docker_container" "web" {
  image = "nginx:latest"
  name  = "web"

  networks_advanced {
    name         = docker_network.private.name
    ipv4_address = "172.20.0.2"
  }
}

resource "docker_container" "app" {
  image = "myapp:latest"
  name  = "app"

  networks_advanced {
    name         = docker_network.private.name
    ipv4_address = "172.20.0.3"
  }
}

# Container communication using hostnames
# web can reach app at: http://app:3000

Multi-network setup:

# Frontend network
resource "docker_network" "frontend" {
  name   = "frontend-network"
  driver = "bridge"
}

# Backend network (isolated)
resource "docker_network" "backend" {
  name   = "backend-network"
  driver = "bridge"
}

# Web tier on frontend
resource "docker_container" "nginx" {
  image = "nginx:latest"
  name  = "nginx"

  networks_advanced {
    name = docker_network.frontend.name
  }
}

# App tier bridges networks
resource "docker_container" "app" {
  image = "myapp:latest"
  name  = "app"

  networks_advanced {
    name = docker_network.frontend.name
  }

  networks_advanced {
    name = docker_network.backend.name
  }
}

# Database tier on backend only
resource "docker_container" "db" {
  image = "postgres:14"
  name  = "postgres"

  networks_advanced {
    name = docker_network.backend.name
  }
}

Docker Volumes

Manage persistent storage for containers.

Create volume:

# Named volume
resource "docker_volume" "app_data" {
  name   = "app-data"
  driver = "local"

  labels = {
    backup = "daily"
  }
}

# Use volume in container
resource "docker_container" "app" {
  image = "myapp:latest"
  name  = "app"

  volumes {
    volume_name    = docker_volume.app_data.name
    container_path = "/app/data"
    read_only      = false
  }
}

# Volume outputs
output "volume_name" {
  value = docker_volume.app_data.name
}

output "volume_mountpoint" {
  value = docker_volume.app_data.mountpoint
}

Bind mount for development:

resource "docker_container" "dev" {
  image = "myapp:latest"
  name  = "app-dev"

  # Bind mount for live code
  volumes {
    host_path      = "${path.module}/app"
    container_path = "/app/src"
    read_only      = false
  }

  # Named volume for dependencies
  volumes {
    volume_name    = docker_volume.node_modules.name
    container_path = "/app/node_modules"
    read_only      = false
  }
}

resource "docker_volume" "node_modules" {
  name = "app-node-modules"
}

Docker Compose Comparison

Terraform vs Docker Compose for container orchestration.

Terraform strengths:

# Define other infrastructure alongside containers
resource "aws_security_group" "app" {
  name = "app-sg"
  # Security group rules
}

resource "docker_container" "app" {
  image = "myapp:latest"
  name  = "app"
  
  # Container with security group reference
  # (demonstrates IaC integration)
}

# Version control and state management
# Easy multi-environment configuration
# Team collaboration with terraform workflows

Docker Compose strengths:

# docker-compose.yml - Simpler for local development
version: '3.8'

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    depends_on:
      - app

  app:
    build: .
    environment:
      - DATABASE_URL=postgres://db:5432/myapp
    depends_on:
      - db

  db:
    image: postgres:14
    environment:
      - POSTGRES_PASSWORD=secret

Using both together:

# Terraform manages images and infrastructure
resource "docker_image" "myapp" {
  build {
    context    = "${path.module}/app"
    dockerfile = "Dockerfile"
  }
}

# Docker Compose runs on developer machines
# Terraform manages production infrastructure

Practical Examples

Web application stack:

# Network
resource "docker_network" "app" {
  name = "app-network"
}

# Database volume
resource "docker_volume" "db_data" {
  name = "postgres-data"
}

# PostgreSQL database
resource "docker_container" "db" {
  image  = "postgres:14"
  name   = "postgres"
  
  env = [
    "POSTGRES_PASSWORD=${var.db_password}",
    "POSTGRES_USER=appuser",
    "POSTGRES_DB=myapp"
  ]
  
  networks_advanced {
    name = docker_network.app.name
  }
  
  volumes {
    volume_name    = docker_volume.db_data.name
    container_path = "/var/lib/postgresql/data"
  }
}

# Application
resource "docker_image" "app" {
  name = "myapp:${var.version}"
  
  build {
    context = "${path.module}/app"
  }
}

resource "docker_container" "app" {
  image = docker_image.app.image_id
  name  = "app"
  
  env = [
    "DATABASE_URL=postgres://appuser:${var.db_password}@postgres:5432/myapp",
    "ENVIRONMENT=${var.environment}"
  ]
  
  networks_advanced {
    name = docker_network.app.name
  }
  
  depends_on = [docker_container.db]
}

# Nginx reverse proxy
resource "docker_container" "nginx" {
  image = "nginx:latest"
  name  = "nginx"
  
  ports {
    internal = 80
    external = 80
  }
  
  volumes {
    host_path      = "${path.module}/nginx.conf"
    container_path = "/etc/nginx/nginx.conf"
  }
  
  networks_advanced {
    name = docker_network.app.name
  }
  
  depends_on = [docker_container.app]
}

Best Practices

Image management:

# Use specific versions
resource "docker_image" "app" {
  name = "myapp:1.0.0"  # Specific version
  # NOT "myapp:latest"
}

# Tag images consistently
resource "docker_image" "app" {
  name = "ghcr.io/myorg/myapp:v${var.app_version}"
}

# Remove dangling images
resource "docker_image" "app" {
  force_remove = true
}

Container management:

# Always set restart policy
resource "docker_container" "app" {
  restart_policy = "on-failure"
  max_retries    = 5
}

# Health checks
resource "docker_container" "app" {
  healthchecks {
    test     = ["CMD", "curl", "-f", "http://localhost:3000"]
    interval = "30s"
    timeout  = "5s"
    retries  = 3
  }
}

# Resource limits
resource "docker_container" "app" {
  memory     = 1024  # 1 GB
  cpus       = 1.0
}

Conclusion

The Terraform Docker provider brings infrastructure-as-code principles to containerized applications, enabling version-controlled, repeatable Docker deployments. By managing containers, networks, and volumes with Terraform, you gain consistency across environments, better collaboration through git-based workflows, and seamless integration with other infrastructure resources. Combine Terraform's Docker provider with your container strategy to build scalable, maintainable containerized infrastructure.