DEV Community

Cover image for From "Works on My Machine" to Infrastructure as Code: Docker, Compose, and Terraform Explained

From "Works on My Machine" to Infrastructure as Code: Docker, Compose, and Terraform Explained


title: "From \"Works on My Machine\" to Infrastructure as Code: Docker, Compose, and Terraform Explained"
published: false
description: "Every deployment tool exists because the previous one left a problem unsolved. Here's the chain of problems behind Docker, Docker Compose, and Terraform."
tags: devops, docker, terraform, beginners

Each tool in modern deployment exists because the previous one left a problem unsolved. Before Docker (released in 2013), deploying an application largely meant setting up servers by hand.

A developer wrote the code, and then came the hardest question: how do we make this application run on the server exactly the way it runs on the developer's machine?

Every answer created a new layer of automation and standardization. Roughly:

Manual deployment → Virtual Machines → Docker → Docker Compose
→ Infrastructure as Code → Terraform → Kubernetes and beyond
Enter fullscreen mode Exit fullscreen mode

To see what each tool is worth, start with the problem it came to solve.

Before Docker: "It works on my machine"

The application was never just code. It depended on an entire environment: the OS, a web server, a specific runtime version, extensions, a database, configuration files, firewall rules, users and permissions.

Take a PHP application. To run, it might need:

  • Linux
  • Apache or Nginx
  • A specific PHP version, plus specific PHP extensions
  • MySQL and Redis
  • System-level settings and configuration files
  • Firewall rules
  • Users and permissions

A developer might run:

Ubuntu / PHP 7.4 / MySQL 5.7 / Apache
Enter fullscreen mode Exit fullscreen mode

While production runs:

CentOS / PHP 7.2 / MySQL 5.6 / Apache with a different configuration
Enter fullscreen mode Exit fullscreen mode

The app works perfectly on the laptop and fails on the server. The bug is not in the code.

It compounds with scale. Each new server means repeating every install step by hand, and one slip creates a different environment. Over time you get Server 1 on configuration A, Server 2 on B, Server 3 on C, across development, staging, and production.

Picture ten servers. Each one needs this chain installed, in order:

OS → Runtime → Libraries → Dependencies → Configuration → Application
Enter fullscreen mode Exit fullscreen mode

Add an eleventh and you repeat all of it. The goal is for development, staging, and production to be as similar as possible, but manual installation pulls them apart a little more with every change.

Virtual Machines: better isolation, high cost

Virtual Machines gave each application its own full operating system on shared hardware, which meant stronger isolation and more consistent environments.

Physical Server
├── VM 1: Ubuntu + Application
├── VM 2: Ubuntu + Application
└── VM 3: Ubuntu + Application
Enter fullscreen mode Exit fullscreen mode

The cost is that every VM carries a complete OS and its own kernel. Across dozens or hundreds of applications, that weight shows up in:

  • Resources (memory and CPU reserved per VM)
  • Startup speed
  • Image size
  • Systems to manage and patch

Containers take a different route: they share the host's kernel and isolate only the application's processes and filesystem. That is why they start in seconds and weigh megabytes instead of gigabytes.

Docker: ship the application with its environment

Docker's idea is simple. Instead of handing over the application and asking the recipient to build the right environment, you package the application, its runtime, and its dependencies into one image, and run that image as a container.

So instead of writing "install Node.js 20, then npm, then these libraries…", you build an image that already contains them:

Application + Dependencies + Runtime + Configuration
                      ↓
                 Docker Image
                      ↓
               Docker Container
Enter fullscreen mode Exit fullscreen mode
Docker Image
├── Node.js
├── Application
├── npm dependencies
└── Configuration
Enter fullscreen mode Exit fullscreen mode

Docker did not solve deployment as a whole. It solved one large problem: a standardized, portable runtime. The same image runs on a laptop, a CI server, staging, and production.

Developer laptop → CI server → Staging → Production
        (the same image at every step)
Enter fullscreen mode Exit fullscreen mode

The application is no longer just source code. It is a clear artifact, the image, that you can build, test, push to a registry, and run anywhere.

Then a new problem appeared. Real applications are rarely one container. They are a frontend, a backend, PostgreSQL, Redis, Nginx, workers, and a message queue, and all of them must find and talk to each other.

            Nginx
              │
              ↓
           Backend
          /       \
         ↓         ↓
    PostgreSQL    Redis
Enter fullscreen mode Exit fullscreen mode

How do you start all of these together, and make sure Nginx reaches the backend, and the backend reaches PostgreSQL and Redis?

Docker Compose: describe the whole stack in one file

Instead of running docker run for each container and creating networks by hand, you describe the stack once in docker-compose.yml: services, networks, volumes, environment variables, dependencies, and ports.

services:
  backend:
    image: my-backend
  redis:
    image: redis
  database:
    image: postgres
Enter fullscreen mode Exit fullscreen mode

Compose creates a Docker network and attaches the services to it, so the backend can reach database and redis by name.

Docker Compose
  ├── Backend
  ├── Redis
  └── PostgreSQL
        └── Docker Network
Enter fullscreen mode Exit fullscreen mode

Docker and Compose are different tools. Docker runs containers. Compose describes and runs a group of containers that together form one application.

Docker          → Container
Docker Compose  → Application stack → Multiple containers
Enter fullscreen mode Exit fullscreen mode

Compose is ideal for local development. It is also a legitimate choice for small production deployments on a single host, though it does not scale across machines or self-heal the way an orchestrator does.

Terraform: infrastructure as code

Compose describes containers, but it does not create what they run on: VPCs, subnets, security groups, servers, load balancers, DNS, storage, databases, IAM. That is infrastructure, and clicking through a cloud console to build it does not scale or leave a record.

Imagine moving the project to production on AWS. Before any container can run, someone must create:

VPC, Subnets, Security Groups, Servers, Load Balancer,
DNS, Storage, Database, IAM, Monitoring
Enter fullscreen mode Exit fullscreen mode

None of that is a container.
Who builds the environment Docker will run on?

Infrastructure as Code (IaC) treats it like software: described in files, reviewed, versioned, and reproducible. The idea predates Terraform. Puppet and Chef automated server configuration, Ansible simplified it, and AWS CloudFormation brought declarative provisioning to one cloud. Terraform's contribution was a single workflow across many providers (AWS, Azure, GCP, Kubernetes, DNS, and more).

Instead of clicking through the console (create VPC, create subnet, create database, create server, create load balancer…), you describe the target in Terraform files and Terraform talks to the provider:

Terraform → AWS / Azure / GCP → Infrastructure
Enter fullscreen mode Exit fullscreen mode

For the AWS example, Terraform could create:

VPC
├── Public Subnet
├── Private Subnet
├── Security Groups
├── Load Balancer
├── EC2
└── RDS PostgreSQL
Enter fullscreen mode Exit fullscreen mode

A minimal example:

resource "aws_db_instance" "main" {
  engine            = "postgres"
  instance_class    = "db.t3.micro"
  allocated_storage = 20
}
Enter fullscreen mode Exit fullscreen mode

Not just networking. Networking is one slice of infrastructure. Depending on the provider, Terraform manages servers, databases, load balancers, DNS, storage, IAM, Kubernetes resources, and other cloud services. The core value is that infrastructure becomes describable, reviewable, reproducible, and changeable like code.

Provisioning vs configuration. Terraform creates infrastructure. It does not decide what runs on your servers afterwards. That is the job of the container layer, cloud-init, or a tool such as Ansible.

State. Terraform records what it manages in a state file (terraform.tfstate), usually stored remotely with locking. It compares your code (desired state) with that state and the real infrastructure, then computes the difference. It reconciles only when you run it; it does not continuously enforce anything.

terraform plan    # show what would change
terraform apply   # make the change
Enter fullscreen mode Exit fullscreen mode

Drift. If someone edits a resource by hand in the console, reality diverges from the code. Terraform detects that on the next plan.

⚠️ Treat state files as sensitive: they can contain secrets.

Documentation by default. Six months into a hand-built production, a new engineer asks why a security group is open on a given port, and the person who opened it left. With Terraform the answer is in the repository, with a commit history.

infrastructure/
├── network.tf
├── database.tf
├── servers.tf
├── loadbalancer.tf
└── variables.tf
Enter fullscreen mode Exit fullscreen mode

Licensing note: since 2023 Terraform is under the Business Source License. OpenTofu, a community fork under the Linux Foundation, is a drop-in alternative for most workflows.

The layers together

Terraform did not replace Docker, and Docker did not replace Terraform. They work at different layers of the same problem.

Tool Answers
Docker Compose What the application is made of
Docker How the application runs
Terraform What it all runs on (the infrastructure)

A typical AWS setup: Terraform creates the VPC, subnets, security groups, load balancer, servers, and database. Docker then runs the application on those servers.

                Terraform
                    │
                    ↓
             AWS Infrastructure
                    │
      ┌─────────────┼────────────┐
      ↓             ↓            ↓
 Load Balancer   Servers        RDS
                    │
                    ↓
                  Docker
                    │
                    ↓
               Application
Enter fullscreen mode Exit fullscreen mode

The database does not have to be a container. In development, Compose can run PostgreSQL next to the backend, which is convenient. In production you may prefer a managed service such as AWS RDS, created by Terraform. The application only needs a connection string (DATABASE_URL), and Docker never creates PostgreSQL.

Development                    Production
Docker Compose                 Docker: Backend
├── Backend                    AWS RDS: PostgreSQL (created by Terraform)
├── PostgreSQL
└── Redis
Enter fullscreen mode Exit fullscreen mode

You do not need all of it

No project needs Docker, Compose, Terraform, and Kubernetes by default. Adopt a tool when you have the problem it solves.

A laptop running Compose with a backend, PostgreSQL, and Redis may not need Terraform at all. Even a database that lives outside Docker as a SaaS service can be reached directly by the application.

Terraform earns its place when you have real infrastructure to create, manage, replicate, and standardize across development, staging, and production. Each of those has its own network, servers, database, load balancer, security rules, and storage, and managing that by hand becomes risky and expensive.

  • A small app on one host: Docker Compose is enough.
  • Real cloud infrastructure and several environments: Docker plus Terraform.
  • Dozens of services, scaling, and self-healing: containers, Kubernetes, Terraform, and CI/CD.

Kubernetes orchestrates containers across many machines: scheduling, scaling, restarts, rollouts.

CI/CD automates building, testing, and deploying every change, so releases stop depending on one person's checklist.

Seen as a progression of problems and answers:

  1. Code alone. Problem: the environment differs from machine to machine.
  2. Docker. Solution: a standardized, portable runtime.
  3. Docker Compose. Solution: a described stack of containers, networks, and volumes.
  4. Terraform. Solution: the infrastructure itself, managed as code.

Conclusion: a chain of problems, not a set of tools

The best way to understand DevOps is not to memorize tool names but to ask what problem each tool came to solve.

Problem Tool
"It only works on my machine" Docker
"I have dozens of containers" Docker Compose
"Where does all of this run?" Terraform
"How do I manage hundreds of them?" Kubernetes
"How do I ship changes automatically?" CI/CD

Docker did not fail when Compose appeared, and Compose did not fail when Terraform appeared. Each tool addressed a new layer of complexity.

Docker manages how the application runs, Compose describes what it is made of, and Terraform manages the infrastructure it lives on. Together they move a team from merely running an application to operating a platform that is repeatable, scalable, and managed as code.

Top comments (0)