# How to deploy an application to a Quake AI VM from CI

Source: https://docs.quake.ai/docs/automation/how-to/app-cicd-vm
Markdown: https://docs.quake.ai/docs/automation/how-to/app-cicd-vm.md

---

# How to deploy an application to a Quake AI VM from CI

Ship application changes from GitHub Actions or GitLab CI to a Quake AI instance over SSH. Quake AI does not provide hosted CI runners. Use GitHub-hosted or GitLab.com runners by default, or run a self-hosted runner on a Quake AI VM when you need private network access.



This page covers **application** deploys (code, containers, scripts). For OpenTofu plan/apply in CI, see [How to integrate OpenTofu with CI/CD](/docs/automation/how-to/cicd-integration).



<PrerequisiteBlock>

- A running [instance](/docs/compute/how-to/create-instance) with SSH access
- An [SSH key](/docs/tools/add-ssh-key) registered in your project and installed on the instance
- Repository secrets configured in GitHub or GitLab

</PrerequisiteBlock>

## Store CI secrets

Add these repository secrets (names are examples; match your workflow):

| Secret | Purpose |
|---|---|
| `SSH_PRIVATE_KEY` | Private key that matches the public key on the instance |
| `DEPLOY_HOST` | Floating IP or hostname of the target instance |
| `DEPLOY_USER` | SSH user on the instance (`ubuntu`, `debian`, etc.) |

For post-deploy OpenStack API checks, add [application credential](/docs/tools/generate-app-credentials) variables using the same `OS_*` shape as the [OpenTofu CI/CD guide](/docs/automation/how-to/cicd-integration#secrets-configuration).

## Pattern A: Rsync and restart over SSH




```yaml
name: Deploy to VM
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install SSH key
        run: |
          install -m 700 -d ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts
      - name: Rsync application
        run: |
          rsync -az --delete ./ "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/opt/myapp/"
      - name: Restart service
        run: |
          ssh "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \
            'sudo systemctl restart myapp'
```




```yaml
deploy:
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client rsync
    - install -m 700 -d ~/.ssh
    - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
    - chmod 600 ~/.ssh/id_ed25519
    - ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
  script:
    - rsync -az --delete ./ "$DEPLOY_USER@$DEPLOY_HOST:/opt/myapp/"
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" 'sudo systemctl restart myapp'
  only:
    - main
```






These workflows assume a **fresh CI runner** with no existing SSH config. On your laptop, `~/.ssh` already holds keys and a config file. Do not paste the `install -m … -d ~/.ssh` block into a local terminal: mode `600` on the directory strips the execute bit and breaks SSH on your machine. For local validation walks, write `known_hosts` under the walk working directory and pass `-o UserKnownHostsFile=…` to `ssh` and `rsync`.



## Pattern B: Build a container image and restart on the VM

Build the image in CI, save it as a tarball or push to a [third-party registry](/docs/kubernetes/how-to/use-container-registry), then SSH to the instance to `docker load` or `docker pull` and restart the container.



When the target instance sits on a private subnet, run a GitHub Actions self-hosted runner or GitLab runner on a bastion or tooling VM inside the project. The runner reaches private addresses without exposing SSH to the public internet. See [SSH bastion access](/docs/network/how-to/ssh-bastion-access).



## See also

- [How to integrate OpenTofu with CI/CD](/docs/automation/how-to/cicd-integration)
- [How to deploy a Docker Compose application on a VM](/docs/compute/how-to/deploy-docker-compose)
- [How to store application secrets and inject them at runtime](/docs/security/how-to/inject-app-secrets)
- [Add an SSH key](/docs/tools/add-ssh-key)
