Skip to content

Migrate a Docker container app from AWS to Quake AI

Migration · Updated Jul 2026

Coming from another cloud?

▸AWS·ECS, Fargate, ECR

This Quake AI feature maps to AWS’s ECS, Fargate, ECR.

Migrate a Docker container app from AWS to Quake AI

You use this guide to move a containerized application from AWS (ECS, Fargate, or EC2 with Docker) to Quake AI (OpenStack Nova). The migration shifts you from managed orchestration to self-managed containers on VMs. You control the runtime, networking, and scaling directly.

If you have not reviewed how AWS concepts map to Quake AI, read Coming from AWS first. For VM-level migration (non-containerized workloads), see Migrate from EC2 to Quake AI Compute.

Prerequisites#

  • A Quake AI account with application credentials
  • OpenTofu (or Terraform) installed and configured with the openstack provider
  • The OpenStack CLI installed (for verification and ad-hoc commands)
  • SSH access configured (add your SSH key)
  • Docker installed on your local machine (for image pull/push operations)
  • Access to your AWS account with permissions to pull images from ECR

ECS and fargate concept translation#

AWS wraps containers in managed abstractions. On Quake AI, you work directly with Docker (or Kubernetes) on Nova instances. The table below maps each AWS container concept to its Quake AI equivalent.

AWS conceptQuake AI equivalentNotes
ECS task definitiondocker-compose.yml or systemd unitPort mappings, environment variables, volumes, health checks, and resource limits translate directly to Compose service definitions
ECS serviceDocker Compose service with restart: unless-stoppedFor Kubernetes: a Deployment with replica count
Fargate (serverless containers)Nova instance + DockerNo serverless container equivalent. You provision and manage the VM, with full SSH access and runtime control.
AWS Lambda (event handlers, background workers)Docker container or systemd timerNo serverless function runtime. Replace Lambda with a container or a script triggered by a queue, scheduled cron, or systemd timer.
ECR (container registry)Docker Hub, GitHub Container Registry (GHCR), or self-hosted registryPush images to a portable registry before migrating. No vendor-specific authentication required on the destination.
ALB + target groupsSelf-managed reverse proxy (Caddy, nginx, HAProxy, or Traefik), or an external CDN and WAFUse a containerized reverse proxy on one VM, or route an external edge service to floating IPs on multiple VMs.
ECS service discoveryDocker Compose DNS (automatic between services) or Consul/CoreDNS for multi-hostContainers in the same Compose file resolve each other by service name.
CloudWatch Container InsightsSelf-managed Prometheus + cAdvisor + GrafanaSee Monitoring for guidance on observability patterns.
AWS Secrets Manager / Parameter StoreEnvironment variables, Docker secrets, or HashiCorp VaultFor Compose: use .env files (not committed to version control) or Docker secrets for swarm mode.
ECS Execdocker exec over SSHSSH into the Nova instance, then docker exec -it CONTAINER_NAME /bin/sh.
ECR lifecycle policiesRegistry-specific cleanup rules or manual docker image pruneDocker Hub and GHCR have retention policies. Self-hosted registries need manual or scripted cleanup.
CloudFormation / CDK (container infra)OpenTofu with the openstack providerSee Migrate from CloudFormation for the full IaC translation.

Choose your deployment target#

Quake AI does not have a managed container service. You run containers on Nova instances using one of three approaches. Pick the one that matches your operational complexity and scaling needs.

ApproachBest forComplexityScaling model
Single VM + Docker ComposeSmall-to-medium apps with fewer than ~10 containers. Most ECS migrations start here.LowVertical: resize the VM flavor. Horizontal: add VMs behind an external edge or self-managed reverse-proxy tier.
Multiple VMs + Docker ComposeWorkloads that need isolation between services (separate DB host, separate worker host) or independent scaling.MediumHorizontal: each VM scales independently. Use a private network for inter-VM traffic.
Self-managed Kubernetes (k3s, RKE2, kubeadm)Teams already operating Kubernetes who want pod orchestration, rolling deploys, and auto-scaling.HighKubernetes-native: replica sets, HPA, cluster autoscaler.

If you are migrating from ECS or Fargate, start with the single-VM Compose approach unless your workload already exceeds what a single m2a.4xlarge (16 vCPU, 64 GiB) can handle. You can promote to multi-VM or Kubernetes later without changing your container images.

If you are migrating from EKS, use the EKS to Kubernetes on Quake AI guide instead.

Migration workflow#

1. Export images from ECR#

Pull every image your task definitions reference, retag them for a portable registry, and push. Replace the placeholders with your values.

bash
# Authenticate to ECR
aws ecr get-login-password --region AWS_REGION | \
  docker login --username AWS --password-stdin \
  AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com

# Pull, retag, push for each image
docker pull AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/APP_NAME:TAG
docker tag AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/APP_NAME:TAG \
  REGISTRY_HOST/REGISTRY_NAMESPACE/APP_NAME:TAG
docker push REGISTRY_HOST/REGISTRY_NAMESPACE/APP_NAME:TAG

Repeat for every image in your task definitions. If you use multi-architecture builds (linux/amd64 + linux/arm64), push the manifest list. Quake AI compute runs AMD64.

2. Translate the task definition to docker-compose.yml#

An ECS task definition defines containers, ports, environment variables, volumes, health checks, and resource limits in JSON. The equivalent on Quake AI is a docker-compose.yml file.

Example ECS task definition (abbreviated):

JSON
{
  "family": "web-app",
  "networkMode": "awsvpc",
  "containerDefinitions": [
    {
      "name": "api",
      "image": "AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/api:v2.1",
      "portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],
      "environment": [
        { "name": "DATABASE_URL", "value": "postgres://db:5432/app" },
        { "name": "REDIS_URL", "value": "redis://cache:6379" }
      ],
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": { "awslogs-group": "/ecs/web-app" }
      }
    },
    {
      "name": "cache",
      "image": "redis:7-alpine",
      "portMappings": [{ "containerPort": 6379 }]
    }
  ]
}

Equivalent docker-compose.yml for Quake AI:

YAML
services:
  api:
    image: REGISTRY_HOST/REGISTRY_NAMESPACE/api:v2.1
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: "postgres://db:5432/app"
      REDIS_URL: "redis://cache:6379"
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
    restart: unless-stopped
    depends_on:
      - cache

  cache:
    image: redis:7-alpine
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: app
      POSTGRES_PASSWORD: "${DB_PASSWORD}"
    restart: unless-stopped

volumes:
  db_data:

The Compose file adds a db service that was not in the ECS task definition. On AWS, the database likely runs on RDS. On Quake AI, you run it as a container alongside your application or on a separate VM. Adjust the service list to match your actual stack.

Translation rules:

ECS task definition fielddocker-compose.yml equivalent
containerDefinitions[].imageservices.SERVICE.image (update the registry URL)
portMappings[].containerPortservices.SERVICE.ports (HOST_PORT:CONTAINER_PORT)
environment[]services.SERVICE.environment (key-value pairs)
secrets[] (from Secrets Manager or SSM Parameter Store)services.SERVICE.env_file pointing to a .env file on disk, or Docker secrets for sensitive values. .env files store plaintext; prefer Docker secrets or HashiCorp Vault for production workloads.
healthCheckservices.SERVICE.healthcheck (same structure, different syntax)
logConfiguration (awslogs or awsfirelens)Docker logging driver (see logging section). For awslogs, use the JSON file driver. For awsfirelens, run Promtail, Fluentd, or Vector as a sidecar container.
cpu / memoryservices.SERVICE.deploy.resources.limits (optional; omit for single-app VMs)
volumes[].host.sourcePathservices.SERVICE.volumes (bind mounts or named volumes)
dependsOnservices.SERVICE.depends_on

3. Provision infrastructure and deploy with OpenTofu#

OpenTofu is the direct replacement for CloudFormation and CDK. It provisions the VM, network, security groups, and block storage, then cloud-init installs Docker and starts your containers on first boot. Your docker-compose.yml travels unchanged inside the cloud-init payload.

For the full IaC setup walkthrough, see Get started with Infrastructure as Code on Quake AI. For CloudFormation-specific resource mapping, see Migrate from AWS CloudFormation.

Create a main.tf that provisions everything your containers need:

HCL
terraform {
  required_providers {
    openstack = {
      source = "terraform-provider-openstack/openstack"
    }
  }
}

variable "app_name" {
  default = "web-app"
}

variable "image" {
  default = "Ubuntu-24.04"
}

variable "flavor" {
  default = "m2a.xlarge"
}

variable "network" {
  type = string
}

variable "key_name" {
  type = string
}

variable "db_password" {
  type      = string
  sensitive = true
}

resource "openstack_networking_secgroup_v2" "app" {
  name        = "${var.app_name}-sg"
  description = "Security group for ${var.app_name}"
}

resource "openstack_networking_secgroup_rule_v2" "ssh" {
  security_group_id = openstack_networking_secgroup_v2.app.id
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = "0.0.0.0/0"
}

resource "openstack_networking_secgroup_rule_v2" "app_port" {
  security_group_id = openstack_networking_secgroup_v2.app.id
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 8080
  port_range_max    = 8080
  remote_ip_prefix  = "0.0.0.0/0"
}

resource "openstack_blockstorage_volume_v3" "data" {
  name = "${var.app_name}-data"
  size = 50
}

resource "openstack_networking_port_v2" "app" {
  network_id         = var.network
  security_group_ids = [openstack_networking_secgroup_v2.app.id]
}

resource "openstack_compute_instance_v2" "app" {
  name        = var.app_name
  image_name  = var.image
  flavor_name = var.flavor
  key_pair    = var.key_name

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

  network {
    port = openstack_networking_port_v2.app.id
  }
}

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

resource "openstack_networking_floatingip_v2" "app" {
  pool = "PublicStatic"
}

resource "openstack_networking_floatingip_associate_v2" "app" {
  floating_ip = openstack_networking_floatingip_v2.app.address
  port_id     = openstack_networking_port_v2.app.id
}

output "floating_ip" {
  value = openstack_networking_floatingip_v2.app.address
}

Adjust port_range_min / port_range_max to match the ports your containers expose. Add additional security group rules for HTTPS (443) or any other public ports. Attach groups to the Neutron port with security_group_ids, not to the instance by name. See Authoring IaC templates. See the flavor mapping table in the EC2 migration guide to match your current AWS instance size to a Quake AI flavor.

4. Deploy containers with cloud-init#

Cloud-init runs on first boot to install Docker, mount the data volume, write your docker-compose.yml, and start the stack. Create a cloud-init.yaml alongside your main.tf:

YAML
#cloud-config
package_update: true

write_files:
  - path: /opt/app/docker-compose.yml
    content: |
      services:
        api:
          image: REGISTRY_HOST/REGISTRY_NAMESPACE/api:v2.1
          ports:
            - "8080:8080"
          environment:
            DATABASE_URL: "postgres://db:5432/app"
            REDIS_URL: "redis://cache:6379"
          healthcheck:
            test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
            interval: 30s
            timeout: 5s
            retries: 3
          restart: unless-stopped
          depends_on:
            - cache
            - db

        cache:
          image: redis:7-alpine
          restart: unless-stopped

        db:
          image: postgres:16-alpine
          volumes:
            - /mnt/app-data/pgdata:/var/lib/postgresql/data
          environment:
            POSTGRES_DB: app
            POSTGRES_PASSWORD: "${db_password}"
          restart: unless-stopped

  - path: /etc/docker/daemon.json
    content: |
      {
        "log-driver": "json-file",
        "log-opts": { "max-size": "50m", "max-file": "3" }
      }

runcmd:
  - curl -fsSL https://get.docker.com | sh
  - mkfs.ext4 /dev/vdb || true
  - mkdir -p /mnt/app-data/pgdata
  - mount /dev/vdb /mnt/app-data
  - echo '/dev/vdb /mnt/app-data ext4 defaults 0 2' >> /etc/fstab
  - cd /opt/app && docker compose pull && docker compose up -d

Replace the docker-compose.yml content with the Compose file you translated in step 2. The ${db_password} variable is injected by OpenTofu's templatefile() function from your terraform.tfvars.

Apply the configuration:

bash
tofu init
tofu plan
tofu apply

OpenTofu provisions the VM, attaches the block volume, and associates the floating IP. Cloud-init installs Docker, mounts the volume, and starts your containers. No manual SSH required for the initial deploy.

Verify the deployment by SSHing into the instance:

bash
ssh ubuntu@$(tofu output -raw floating_ip)
docker compose -f /opt/app/docker-compose.yml ps

Every service should show Up with its health status. If a container fails to start, check its logs:

bash
docker compose -f /opt/app/docker-compose.yml logs SERVICE_NAME

For a deeper walkthrough of the OpenTofu + cloud-init pattern, including multi-service topologies and volume management, see Migrate from Docker Compose to OpenTofu on Quake AI.

5. Configure networking and traffic routing#

Single-VM deployment: Your security group rules and floating IP handle public access. Traffic reaches the VM on the published port, and Docker's port mapping routes it to the container.

Multi-VM deployment: Attach floating IPs to the public application or proxy instances. Put an external CDN or WAF with health-checked origins in front of those addresses, or run a dedicated reverse-proxy tier that reaches application instances over a private network.

Self-managed reverse proxy: For HTTPS termination, domain routing, or path-based routing on a single VM, add a reverse proxy container (Caddy, nginx, or Traefik) to your Compose file:

YAML
services:
  reverse-proxy:
    image: caddy:2-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
    restart: unless-stopped

volumes:
  caddy_data:

This replaces the ALB. Caddy handles automatic HTTPS via Let's Encrypt; the only configuration is the Caddyfile.

6. Set up health checks and auto-restart#

Docker Compose restart: unless-stopped ensures containers restart after crashes and after VM reboots. The healthcheck directive (translated from your ECS task definition in step 2) lets Docker track container health. If you deployed via cloud-init (step 4), the runcmd block already starts your stack on first boot, and the restart policy keeps it running across reboots.

If you deployed manually (without cloud-init) and need boot-time startup, create a systemd unit:

bash
sudo tee /etc/systemd/system/app-compose.service > /dev/null <<'UNIT'
[Unit]
Description=App Docker Compose
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/app
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable app-compose.service

7. Set up logging#

ECS sends container logs to CloudWatch by default. On Quake AI, configure Docker's logging driver.

Option A: JSON file driver (default). Logs are stored on the VM at /var/lib/docker/containers/. Use docker compose logs to read them. The cloud-init template in step 4 already configures log rotation in /etc/docker/daemon.json. If you deployed manually, add log rotation to prevent disk exhaustion:

JSON
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "3"
  }
}

Save this as /etc/docker/daemon.json and restart Docker (sudo systemctl restart docker).

Option B: Ship to a central stack. Run Promtail, Fluentd, or Vector as a sidecar container to forward logs to Loki, Elasticsearch, or a similar backend. See Monitoring for observability patterns on Quake AI.

Migrate application data#

File-based data#

Transfer files from your ECS host or EFS mount to the Quake AI instance:

bash
rsync -avz --progress -e "ssh -i SSH_KEY_PATH" \
  LOCAL_DATA_PATH ubuntu@FLOATING_IP:/opt/app/data/

Databases#

If your containers connect to RDS, migrate the database to a self-managed instance on Quake AI. Dump from the source, restore to the destination.

Before restoring, confirm the db container is running on the Quake AI instance (docker compose ps). For RDS instances without a public endpoint, run the dump from an EC2 instance in the same VPC and pipe to the Quake AI target.

bash
# PostgreSQL
pg_dump -h RDS_ENDPOINT -U DB_USER DB_NAME | \
  ssh ubuntu@FLOATING_IP "docker exec -i db psql -U DB_USER DB_NAME"

# MySQL
mysqldump -h RDS_ENDPOINT -u DB_USER -p DB_NAME | \
  ssh ubuntu@FLOATING_IP "docker exec -i db mysql -u DB_USER -p DB_NAME"

Persistent volumes#

The OpenTofu configuration in step 3 already provisions and attaches a Cinder block volume, and the cloud-init script formats, mounts, and adds it to /etc/fstab. The Compose file bind-mounts the volume path into your container:

YAML
services:
  db:
    volumes:
      - /mnt/app-data/pgdata:/var/lib/postgresql/data

To resize the volume later, update the size attribute in openstack_blockstorage_volume_v3 and run tofu apply. See Create and attach a block volume for the full guide.

Validation checklist#

After deploying on Quake AI, verify:

  • All containers report healthy (docker compose ps shows Up (healthy))
  • Application responds on the expected ports through the floating IP
  • Security group rules allow the same traffic patterns as your AWS configuration
  • Data integrity: compare row counts, file checksums, or application-level validation
  • DNS records updated to point to the Quake AI floating IP (use low TTLs during transition)
  • Monitoring in place: container metrics, logs, and application-level health
  • Auto-restart works: reboot the VM and confirm all containers come back up
  • SSL/TLS certificates installed and renewing (if using HTTPS)
  • Environment variables and secrets loaded correctly (no hardcoded AWS-specific values)

What changes, what stays the same#

AspectChangesStays the same
Container imagesRegistry URL (ECR → portable registry)Dockerfile, build process, image contents
OrchestrationECS/Fargate → Docker Compose on a VMContainer networking, port mappings, health checks
InfrastructureCloudFormation/CDK → OpenTofu with the openstack providerInfrastructure as Code workflow (tofu plan / tofu apply)
NetworkingALB + target groups → security groups, floating IPs, and an external edge or self-managed reverse proxyContainer-to-container DNS within Compose
StorageEBS/EFS → Cinder block volumesDocker volume mounts inside the VM
LoggingCloudWatch → JSON file driver or self-managed log stackApplication log output (stdout/stderr)
SecretsSecrets Manager → .env files, Docker secrets, or VaultEnvironment variable consumption in containers
ScalingECS auto-scaling → manual VM resize or add VMs behind an external edge or reverse-proxy tierHorizontal scaling via multiple container replicas
Cost modelPer-task/per-vCPU-second billing → per-VM hourly billingPredictable: no burst-traffic surcharges

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

Quick answers

Was this page helpful?