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.

Table of Contents

  1. Docker Provider Overview
  2. Provider Configuration
  3. Managing Docker Images
  4. Managing Containers
  5. Docker Networks
  6. Docker Volumes
  7. Docker Compose Comparison
  8. Practical Examples
  9. Best Practices
  10. Conclusion

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.