How to Deploy a Containerized Application with Docker Compose on a Quake AI VM
Coming from another cloud?
▸AWS·ECS, Fargate
This Quake AI feature maps to AWS’s ECS, Fargate.
▸DigitalOcean·APP Platform ENV Vars
This Quake AI feature maps to DigitalOcean’s APP Platform ENV Vars.
How to deploy a containerized application with Docker Compose on a Quake AI VM
Run a multi-container application from a docker-compose.yml file on a Quake AI virtual machine. You install Docker and the Compose plugin on the VM, move your project onto it, supply environment variables and secrets, start the stack, expose it to the internet, and keep it running across reboots.
Prerequisites
- CLIOpenStack CLI installed and authenticated (
clouds.yamloropenrcsourced)
Windows: CLI examples use bash. Set up a Linux CLI environment on Windows before proceeding.
- A running Ubuntu VM with SSH access. Create one with a private network and a floating IP, or follow Create an instance.
- Exactly one floating IP attached to the VM, so you can reach the application over the public internet.
- An SSH key pair uploaded to your account and loaded in your local agent.
- Your application's
docker-compose.yml(orcompose.yaml) file, plus any images it references published to a registry the VM can pull from.
Step 1. Install Docker and the Compose plugin#
This guide assumes an Ubuntu base image. Install Docker Engine and the Compose plugin from Docker's official repository, which provides the docker compose subcommand:
SSH into your VM:
ssh ubuntu@YOUR_FLOATING_IPInstall Docker using the official repository:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-pluginAdd your user to the Docker group so you can run commands without sudo:
sudo usermod -aG docker ubuntu
newgrp dockerGroup membership changes take effect on your next login. The newgrp docker line above activates the group in your current shell. If you skip it or open a new shell, log out and SSH back in before you run docker commands.
Confirm the install with a command that needs the daemon socket:
docker run --rm hello-worldA daemon-less check such as docker --version passes even when the group is not active yet, so it hides this gotcha.
Confirm the Compose plugin is present:
docker compose versionThe command prints the installed Compose version, for example Docker Compose version v2.27.0.
After a fresh install, the login user is not in the docker group yet, so bare docker compose returns a permission error on docker.sock. Add the user to the group and start a new SSH session before Step 4:
sudo usermod -aG docker $USER
exitSSH back in, then confirm docker compose version runs without sudo.
Step 2. Transfer your project to the VM#
Move the project directory that holds your docker-compose.yml onto the VM. Use whichever source matches your workflow.
If your project lives in a Git repository, clone it on the VM after you SSH in:
git clone https://github.com/YOUR_ORG/YOUR_APP.git
cd YOUR_APPIf your project lives on your local machine, copy it over with scp from your workstation, then SSH in:
scp -r ./YOUR_APP ubuntu@YOUR_FLOATING_IP:~/Confirm the Compose file sits in the current directory before you continue:
ls docker-compose.ymlStep 3. Configure environment variables and secrets#
Docker Compose reads a file named .env in the project directory and substitutes its values into the Compose file at runtime. Create the .env file on the VM and restrict its permissions:
cat > .env <<'EOF'
POSTGRES_PASSWORD=REPLACE_WITH_A_STRONG_VALUE
API_TOKEN=REPLACE_WITH_A_STRONG_VALUE
EOF
chmod 600 .envReference the variables in your Compose file with ${VARIABLE_NAME} syntax:
services:
api:
image: REGISTRY_HOST/REGISTRY_NAMESPACE/api:1.0
environment:
DATABASE_URL: "postgres://app:${POSTGRES_PASSWORD}@db:5432/app"
API_TOKEN: "${API_TOKEN}"
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"Add .env to your .gitignore so secrets never reach version control.
If your images live in a private registry, authenticate before you start the stack so Docker can pull them:
echo "$REGISTRY_TOKEN" | docker login REGISTRY_HOST --username REGISTRY_USER --password-stdinStep 4. Start the stack#
Start every service defined in the Compose file in the background:
docker compose up -dDocker pulls the images, creates a network for the project, and starts each container. List the services to confirm they are running:
docker compose psEach service shows a State of running (and healthy once any health check passes). To read the output of a service that fails to start, follow its logs:
docker compose logs -f SERVICE_NAMEStep 5. Expose the application#
A single-VM deployment uses exactly one floating IP, attached to the VM in the prerequisites. Traffic reaches the VM on that address, and Docker's port mapping forwards it to the container.
Open the ports your application serves in the VM's security group. For a web application, allow TCP 80 and 443. Follow Create security group rules to add them, then publish the matching container ports in your Compose file:
services:
api:
ports:
- "80:8080"For HTTPS termination, domain routing, or path-based routing on a single VM, add a reverse proxy container. Caddy requests and renews certificates from Let's Encrypt on its own once a domain's DNS A record points at the floating IP:
services:
proxy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
restart: unless-stopped
volumes:
caddy_data:Define the routing in a Caddyfile next to your Compose file:
app.example.com {
reverse_proxy api:8080
}If you run the application across multiple VMs, put an external CDN or WAF with origin health checks in front of their floating IPs. You can also run Caddy, nginx, HAProxy, or Traefik on dedicated proxy VMs and route traffic to application VMs over a private network.
Step 6. Restart the stack on boot with systemd#
Containers that set restart: unless-stopped come back after the Docker daemon starts, including after a VM reboot. To manage the whole project as a host service that you can start, stop, and inspect through systemctl, add a systemd unit. Replace the working directory with the absolute path to your project:
sudo tee /etc/systemd/system/compose-app.service > /dev/null <<'UNIT'
[Unit]
Description=Application managed by Docker Compose
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/home/ubuntu/YOUR_APP
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
[Install]
WantedBy=multi-user.target
UNITReload systemd, then enable and start the unit:
sudo systemctl daemon-reload
sudo systemctl enable --now compose-app.serviceCheck that systemd reports the unit as active:
systemctl status compose-app.serviceVerify the deployment#
Confirm the application responds from the VM itself:
curl -I http://localhost:8080A healthy service returns an HTTP status line such as HTTP/1.1 200 OK. From your workstation, request the floating IP or the domain you configured to confirm public access works.
Reboot the VM to verify the stack returns without manual steps:
sudo rebootAfter the VM comes back, SSH in and check the services:
docker compose psEvery service reports running, which confirms the restart policy and the systemd unit bring the stack up on boot.
See also#
- How to use a container registry with Quake AI: authenticate Docker pulls on the VM
- How to store application secrets and inject them at runtime: supply Compose env files without committing secrets
- How to deploy an application to a Quake AI VM from CI: automate
docker compose pulland restart from CI - How to point a domain at a Quake AI resource: map a hostname to the VM's floating IP
- Migrate a Docker container app from AWS to Quake AI: translate ECS task definitions and move images to a portable registry
- Migrate from Docker Compose to OpenTofu: codify a Compose deployment as Infrastructure as Code with cloud-init
- Deploy a containerized web application: swap, resource limits, and multi-container patterns on smaller VMs
- Create a VM on a private network: provision the host used in this guide
- Allocate floating IPs: attach the single public address this pattern requires
- Monitoring: observability patterns for containers on Quake AI
- Compute CLI reference and Compute API reference
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.
For the full policy, see Usage Guidelines.
Last validated: 18.06.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