# How to migrate from Docker Compose to OpenTofu on Quake AI

Source: https://docs.quake.ai/docs/automation/migration/from-docker-compose
Markdown: https://docs.quake.ai/docs/automation/migration/from-docker-compose.md

---

# How to migrate from Docker Compose to OpenTofu on Quake AI

Docker Compose defines multi-container applications on a single host. Moving to Quake AI with OpenTofu means shifting from container orchestration to infrastructure provisioning: the containers still run, but the underlying VMs, networks, and storage are now codified.

This guide covers the most common path: deploying a Docker Compose application on Quake AI compute instances provisioned and configured with OpenTofu and cloud-init.

## Conceptual mapping

| Docker Compose concept | Quake AI OpenTofu equivalent |
|---|---|
| `services:` (container definitions) | `openstack_compute_instance_v2` + cloud-init to install Docker and run containers |
| `ports:` (published ports) | `openstack_networking_secgroup_rule_v2` (security group rules) + floating IP |
| `volumes:` (named volumes) | `openstack_blockstorage_volume_v3` (persistent NVMe block storage) |
| `networks:` (Docker networks) | `openstack_networking_network_v2` + `openstack_networking_subnet_v2` |
| `depends_on:` (service ordering) | OpenTofu resource dependencies (implicit or `depends_on`) |
| `.env` file | `terraform.tfvars` or environment variables |

## The pattern: OpenTofu provisions, cloud-init configures

OpenTofu creates the infrastructure (VMs, networks, volumes, security groups). Cloud-init runs on first boot to install Docker and start your containers. Your `docker-compose.yml` file travels unchanged; it runs inside the VM.

### Example: WordPress + MySQL

If your current `docker-compose.yml` looks like this:

```yaml
services:
  wordpress:
    image: wordpress:latest
    ports:
      - "80:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
    depends_on:
      - db

  db:
    image: mysql:8
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}

volumes:
  db_data:
```

The OpenTofu equivalent on Quake AI uses a compute instance with cloud-init. Attach security groups to the Neutron port (`security_group_ids`), not to the instance by name. See [Authoring IaC templates](/docs/automation/concepts/authoring-iac-templates).

```hcl
data "openstack_images_image_v2" "ubuntu" {
  name        = "Ubuntu-24.04"
  most_recent = true
}

resource "openstack_networking_secgroup_v2" "web" {
  name        = "wordpress-web"
  description = "Ingress for WordPress and SSH"
}

resource "openstack_networking_port_v2" "app" {
  network_id         = openstack_networking_network_v2.main.id
  security_group_ids = [openstack_networking_secgroup_v2.web.id]
}

resource "openstack_compute_instance_v2" "app" {
  name        = "wordpress-app"
  flavor_name = "s1a.medium"

  block_device {
    uuid                  = data.openstack_images_image_v2.ubuntu.id
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 20
    boot_index            = 0
    delete_on_termination = true
  }

  user_data = templatefile("cloud-init.yaml", {
    db_password = var.db_password
  })

  network {
    port = openstack_networking_port_v2.app.id
  }
}

resource "openstack_blockstorage_volume_v3" "db_data" {
  name = "wordpress-db-data"
  size = 20
}

resource "openstack_compute_volume_attach_v2" "db_data" {
  instance_id = openstack_compute_instance_v2.app.id
  volume_id   = openstack_blockstorage_volume_v3.db_data.id
}
```

The cloud-init script installs Docker, mounts the volume, and starts the containers:

```yaml
#cloud-config
package_update: true
packages:
  - docker.io
  - docker-compose-v2

write_files:
  - path: /opt/app/docker-compose.yml
    content: |
      services:
        wordpress:
          image: wordpress:latest
          ports:
            - "80:80"
          environment:
            WORDPRESS_DB_HOST: db
            WORDPRESS_DB_PASSWORD: ${db_password}
          depends_on:
            - db
        db:
          image: mysql:8
          volumes:
            - /mnt/db_data:/var/lib/mysql
          environment:
            MYSQL_ROOT_PASSWORD: ${db_password}

runcmd:
  - mkfs.ext4 /dev/vdb || true
  - mkdir -p /mnt/db_data
  - mount /dev/vdb /mnt/db_data
  - echo '/dev/vdb /mnt/db_data ext4 defaults 0 2' >> /etc/fstab
  - systemctl enable docker
  - systemctl start docker
  - cd /opt/app && docker compose up -d
```



Every Quake AI flavor reports zero ephemeral disk (`disk=0`), so the compute instance requires a `block_device` block that boots from a Cinder volume. The cloud-init script below mounts `/dev/vdb` for database storage; pair it with the separate `openstack_blockstorage_volume_v3` attach shown in the sample, or use the [Simple VM template](/resources/iac-templates/simple-vm) for the full private-network topology.




The [WordPress + MySQL template](/resources/iac-templates/wordpress-mysql) implements this pattern as a ready-to-use OpenTofu module.


## Migration workflow

### 1. Inventory your Compose services

List every service, its image, ports, volumes, and environment variables. Identify which services need:
- Public access (floating IP + security group rules)
- Persistent storage (block volumes)
- Private communication only (internal network)

### 2. Choose your topology

| Compose setup | Quake AI topology |
|---|---|
| All services on one host | Single VM with Docker Compose (simplest migration) |
| Services that need isolation | Separate VMs per service group, private network |
| Multiple application hosts | Edge reverse proxy with a floating IP and private backend addresses |

For most small-to-medium Compose stacks, a single VM is the right starting point. Scale to multiple VMs when you need isolation or redundancy.

### 3. Provision with OpenTofu

Use a Quake AI template as your starting point:
- Single-app stack → [Simple VM template](/resources/iac-templates/simple-vm) + cloud-init
- Web + DB → [WordPress + MySQL template](/resources/iac-templates/wordpress-mysql)
- Multi-tier → [Full-Stack Application template](/resources/iac-templates/full-stack-app)
- Multiple application hosts → [Edge Reverse Proxy template](/resources/iac-templates/edge-reverse-proxy) in front of private backends

### 4. Transfer your data

For volume data, copy files from your current host to the Quake AI instance:

```bash
rsync -avz --progress /path/to/local/data user@FLOATING_IP:/mnt/db_data/
```

For database dumps:

```bash
docker exec db mysqldump -u root -p YOUR_DB > dump.sql
scp dump.sql user@FLOATING_IP:/tmp/
ssh user@FLOATING_IP "docker exec -i db mysql -u root -p < /tmp/dump.sql"
```

### 5. Update DNS

Point your domain to the new floating IP. If you used a reverse proxy (Nginx, Caddy, Traefik) in your Compose setup, it runs unchanged inside the VM.

## What changes, what stays the same

| Aspect | Changes | Stays the same |
|---|---|---|
| **Container runtime** | (none) | Docker, same images, same `docker-compose.yml` |
| **Infrastructure** | Managed by OpenTofu instead of manual setup | (none) |
| **Networking** | OpenStack security groups instead of host firewall | Docker internal networking between containers |
| **Storage** | OpenStack block volumes instead of host directories | Docker volume mounts inside the VM |
| **Deployment** | `tofu apply` provisions the VM; cloud-init starts containers | `docker compose up -d` still runs your app |

## See also

- [Migration Explorer](/resources/migration): interactive service comparison across cloud providers
- [WordPress + MySQL template](/resources/iac-templates/wordpress-mysql)
- [How to get started with Infrastructure as Code on Quake AI](/docs/automation/how-to/getting-started-iac)
- [IaC on Quake AI](/docs/automation/concepts/iac-comparison)
- [Infrastructure Templates](/resources/iac-templates)
