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

Source: https://docs.quake.ai/docs/compute/how-to/deploy-docker-compose
Markdown: https://docs.quake.ai/docs/compute/how-to/deploy-docker-compose.md

---

# 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.



Run Docker Compose on a Quake AI virtual machine: install Docker on the VM, copy your Compose file, and start the stack with `docker compose up`. You control the host, runtime, and network. Container workloads on Quake AI run on VMs or Kubernetes clusters you operate.



<PrerequisiteBlock methods={["cli"]}>

- A running Ubuntu VM with SSH access. Create one with [a private network and a floating IP](/docs/compute/how-to/create-vm-private-network), or follow [Create an instance](/docs/compute/how-to/create-instance).
- Exactly [one floating IP](/docs/network/how-to/allocate-floating-ips) attached to the VM, so you can reach the application over the public internet.
- An [SSH key pair](/docs/tools/add-ssh-key) 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.

</PrerequisiteBlock>

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

<InstallDocker username="ubuntu" method="official" />

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.



A `.env` file holds secrets in plaintext on the VM disk. Keep its mode at `600` and limit who can log in to the host. For workloads that need stronger isolation, use [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) or an external secrets manager such as HashiCorp Vault, and inject the values at deploy time rather than writing them to disk.



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](/docs/network/how-to/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:

```text
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

- [How to use a container registry with Quake AI](/docs/kubernetes/how-to/use-container-registry): authenticate Docker pulls on the VM
- [How to store application secrets and inject them at runtime](/docs/security/how-to/inject-app-secrets): supply Compose env files without committing secrets
- [How to deploy an application to a Quake AI VM from CI](/docs/automation/how-to/app-cicd-vm): automate `docker compose pull` and restart from CI
- [How to point a domain at a Quake AI resource](/docs/network/how-to/point-domain-to-quake-ai): map a hostname to the VM's floating IP
- [Migrate a Docker container app from AWS to Quake AI](/docs/compute/migration/migrate-docker-app-from-aws): translate ECS task definitions and move images to a portable registry
- [Migrate from Docker Compose to OpenTofu](/docs/automation/migration/from-docker-compose): codify a Compose deployment as Infrastructure as Code with cloud-init
- [Deploy a containerized web application](/docs/quickstart/deploy-containerized-app): swap, resource limits, and multi-container patterns on smaller VMs
- [Create a VM on a private network](/docs/compute/how-to/create-vm-private-network): provision the host used in this guide
- [Allocate floating IPs](/docs/network/how-to/allocate-floating-ips): attach the single public address this pattern requires
- [Monitoring](/docs/operate/monitoring): observability patterns for containers on Quake AI
- [Compute CLI reference](/reference/compute/cli) and [Compute API reference](/reference/compute/api)
