Skip to content

How to migrate from Docker Compose to OpenTofu on Quake AI

Migration · Updated Jul 2026

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 conceptQuake 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 fileterraform.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.

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

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 setupQuake AI topology
All services on one hostSingle VM with Docker Compose (simplest migration)
Services that need isolationSeparate VMs per service group, private network
Multiple application hostsEdge 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:

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#

AspectChangesStays the same
Container runtime(none)Docker, same images, same docker-compose.yml
InfrastructureManaged by OpenTofu instead of manual setup(none)
NetworkingOpenStack security groups instead of host firewallDocker internal networking between containers
StorageOpenStack block volumes instead of host directoriesDocker volume mounts inside the VM
Deploymenttofu apply provisions the VM; cloud-init starts containersdocker compose up -d still runs your app

See also#

Usage Guidelines

The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.

Comparisons to third-party providers in this material reflect publicly documented behavior as of the validation date below. Pricing, quotas, service limits, and feature availability change frequently on every cloud. Verify provider-specific claims against the provider's own current documentation before relying on them for a procurement, architecture, or migration decision.

For the full policy, see Usage Guidelines.

Last validated: 07.07.2026

Was this page helpful?