Skip to content

How to Deploy a Containerized Application with Docker Compose on a Quake AI VM

How-to · Updated Jun 2026

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

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 (or compose.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_IP

Install 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-plugin

Add your user to the Docker group so you can run commands without sudo:

sudo usermod -aG docker ubuntu
newgrp docker

Group 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-world

A 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:

bash
docker compose version

The 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:

bash
sudo usermod -aG docker $USER
exit

SSH 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:

bash
git clone https://github.com/YOUR_ORG/YOUR_APP.git
cd YOUR_APP

If your project lives on your local machine, copy it over with scp from your workstation, then SSH in:

bash
scp -r ./YOUR_APP ubuntu@YOUR_FLOATING_IP:~/

Confirm the Compose file sits in the current directory before you continue:

bash
ls docker-compose.yml

Step 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:

bash
cat > .env <<'EOF'
POSTGRES_PASSWORD=REPLACE_WITH_A_STRONG_VALUE
API_TOKEN=REPLACE_WITH_A_STRONG_VALUE
EOF
chmod 600 .env

Reference the variables in your Compose file with ${VARIABLE_NAME} syntax:

YAML
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:

bash
echo "$REGISTRY_TOKEN" | docker login REGISTRY_HOST --username REGISTRY_USER --password-stdin

Step 4. Start the stack#

Start every service defined in the Compose file in the background:

bash
docker compose up -d

Docker pulls the images, creates a network for the project, and starts each container. List the services to confirm they are running:

bash
docker compose ps

Each 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:

bash
docker compose logs -f SERVICE_NAME

Step 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:

YAML
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:

YAML
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:

bash
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
UNIT

Reload systemd, then enable and start the unit:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now compose-app.service

Check that systemd reports the unit as active:

bash
systemctl status compose-app.service

Verify the deployment#

Confirm the application responds from the VM itself:

bash
curl -I http://localhost:8080

A 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:

bash
sudo reboot

After the VM comes back, SSH in and check the services:

bash
docker compose ps

Every service reports running, which confirms the restart policy and the systemd unit bring the stack up on boot.

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.

For the full policy, see Usage Guidelines.

Last validated: 18.06.2026

Quick answers

Was this page helpful?