Migrate a Docker container app from AWS to Quake AI
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
openstackprovider - 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 concept | Quake AI equivalent | Notes |
|---|---|---|
| ECS task definition | docker-compose.yml or systemd unit | Port mappings, environment variables, volumes, health checks, and resource limits translate directly to Compose service definitions |
| ECS service | Docker Compose service with restart: unless-stopped | For Kubernetes: a Deployment with replica count |
| Fargate (serverless containers) | Nova instance + Docker | No 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 timer | No 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 registry | Push images to a portable registry before migrating. No vendor-specific authentication required on the destination. |
| ALB + target groups | Self-managed reverse proxy (Caddy, nginx, HAProxy, or Traefik), or an external CDN and WAF | Use a containerized reverse proxy on one VM, or route an external edge service to floating IPs on multiple VMs. |
| ECS service discovery | Docker Compose DNS (automatic between services) or Consul/CoreDNS for multi-host | Containers in the same Compose file resolve each other by service name. |
| CloudWatch Container Insights | Self-managed Prometheus + cAdvisor + Grafana | See Monitoring for guidance on observability patterns. |
| AWS Secrets Manager / Parameter Store | Environment variables, Docker secrets, or HashiCorp Vault | For Compose: use .env files (not committed to version control) or Docker secrets for swarm mode. |
| ECS Exec | docker exec over SSH | SSH into the Nova instance, then docker exec -it CONTAINER_NAME /bin/sh. |
| ECR lifecycle policies | Registry-specific cleanup rules or manual docker image prune | Docker Hub and GHCR have retention policies. Self-hosted registries need manual or scripted cleanup. |
| CloudFormation / CDK (container infra) | OpenTofu with the openstack provider | See 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.
| Approach | Best for | Complexity | Scaling model |
|---|---|---|---|
| Single VM + Docker Compose | Small-to-medium apps with fewer than ~10 containers. Most ECS migrations start here. | Low | Vertical: resize the VM flavor. Horizontal: add VMs behind an external edge or self-managed reverse-proxy tier. |
| Multiple VMs + Docker Compose | Workloads that need isolation between services (separate DB host, separate worker host) or independent scaling. | Medium | Horizontal: 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. | High | Kubernetes-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.
# 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:TAGRepeat 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):
{
"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:
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 field | docker-compose.yml equivalent |
|---|---|
containerDefinitions[].image | services.SERVICE.image (update the registry URL) |
portMappings[].containerPort | services.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. |
healthCheck | services.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 / memory | services.SERVICE.deploy.resources.limits (optional; omit for single-app VMs) |
volumes[].host.sourcePath | services.SERVICE.volumes (bind mounts or named volumes) |
dependsOn | services.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:
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:
#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 -dReplace 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:
tofu init
tofu plan
tofu applyOpenTofu 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:
ssh ubuntu@$(tofu output -raw floating_ip)
docker compose -f /opt/app/docker-compose.yml psEvery service should show Up with its health status. If a container fails to start, check its logs:
docker compose -f /opt/app/docker-compose.yml logs SERVICE_NAMEFor 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:
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:
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.service7. 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:
{
"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:
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.
# 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:
services:
db:
volumes:
- /mnt/app-data/pgdata:/var/lib/postgresql/dataTo 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 psshowsUp (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#
| Aspect | Changes | Stays the same |
|---|---|---|
| Container images | Registry URL (ECR → portable registry) | Dockerfile, build process, image contents |
| Orchestration | ECS/Fargate → Docker Compose on a VM | Container networking, port mappings, health checks |
| Infrastructure | CloudFormation/CDK → OpenTofu with the openstack provider | Infrastructure as Code workflow (tofu plan / tofu apply) |
| Networking | ALB + target groups → security groups, floating IPs, and an external edge or self-managed reverse proxy | Container-to-container DNS within Compose |
| Storage | EBS/EFS → Cinder block volumes | Docker volume mounts inside the VM |
| Logging | CloudWatch → JSON file driver or self-managed log stack | Application log output (stdout/stderr) |
| Secrets | Secrets Manager → .env files, Docker secrets, or Vault | Environment variable consumption in containers |
| Scaling | ECS auto-scaling → manual VM resize or add VMs behind an external edge or reverse-proxy tier | Horizontal scaling via multiple container replicas |
| Cost model | Per-task/per-vCPU-second billing → per-VM hourly billing | Predictable: no burst-traffic surcharges |
See also#
- Migrating from AWS to Quake AI: full cross-service migration hub
- Migrate from EC2: VM-level migration with flavor mapping
- Get started with Infrastructure as Code: OpenTofu setup and
openstackprovider configuration - Migrate from AWS CloudFormation: CloudFormation resource → OpenTofu resource mapping
- Migrate from Docker Compose to OpenTofu: deeper dive on Compose + cloud-init patterns
- Migrate from EKS: Kubernetes-level migration
- Coming from AWS: concept translation reference
- Deploy a containerized web application: swap, resource limits, and multi-container patterns on smaller VMs
- Monitoring: observability patterns on Quake AI
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
- Why does `openstack image save` write a 0-byte file for my boot-from-volume instance?CLI
- Why does `openstack server create` fail with "Only volume-backed servers are allowed for flavors with zero disk"?CLIAPITerraform
- Why does my project still have a 10 GiB Cinder volume after I deleted my instance?CLIAPI
See Also
Deploy self-hosted Supabase with Docker Compose
Shares: Docker, Containers
Containerized App on Compute
Shares: Docker, Containers
Next.js App on Compute
Shares: Docker, Containers
Terraform and OpenTofu on Quake AI
Prerequisite
How to Deploy a Containerized Application with Docker Compose on a Quake AI VM
Shares: Docker, Containers