Deploying

A "deploy" takes your project source, builds it into a container image on the platform, and rolls that image out to a slot. You never build locally and you never write a Dockerfile. There are four ways to start a deploy: the CLI, GitHub Actions, the dashboard upload, and a link to a public GitHub repository.

Via the CLI

velixir deploy

This:

  1. Packages the current directory's source into .velixir/source.tar.gz. Packing happens in-process, so no tar binary and no build tools are needed on your machine.
  2. Detects your language (informational) and asks the platform for a presigned upload URL, then PUTs the archive straight to object storage.
  3. Creates a release and finalises it.
  4. The platform builds the image, enqueues a deployment, and the CLI polls until it lands or fails.

The server-side build is where your dependencies are resolved and your image is assembled, so the first deploy of a project takes a little longer than the ones after it. If the build fails, the failure reason comes back to your terminal and the full build output is on the app's Overview tab.

Deploy to a named slot with velixir deploy --slot staging, or package a different directory with velixir deploy --source-dir ./service.

Make sure it listens, and that we can tell

Your app must listen on 0.0.0.0 at the port in the PORT environment variable. It's 8080 unless you change it under Scaling → Runtime. The health check is off by default, which means a deploy is marked healthy as soon as the container starts, even if nothing is listening. Turn on HTTP health probe in the same place (the default path is /health) and a deploy that never answers fails, while your previous release keeps serving.

Via the dashboard

From an app's Overview tab, click Upload source. Pick a .tar.gz or .zip of your project source: your code plus its manifest, for example package.json, requirements.txt, go.mod, pom.xml, or a .csproj. We sign a one-time URL pointing at object storage; your browser uploads directly to that URL, with no proxy in between, and the build starts as soon as the upload lands.

From a public GitHub repository

On /new, choose Deploy a public GitHub repo and paste its link. velixir downloads the repository itself, works out what it is built with the same way the CLI does, and shows you the commit it is about to deploy. Pick a plan, create the app, and it builds.

  • Branches and folders. Paste the link to a branch (github.com/you/app/tree/dev) to deploy that branch, or to a folder (github.com/you/monorepo/tree/main/apps/web) to deploy just that folder of a monorepo, as the top of the build.
  • Redeploying. The app remembers the repository, branch and folder. Deploy latest commit on its Deploys tab fetches the newest commit and deploys it. Nothing watches the repository for pushes: to deploy on every push, add the GitHub Actions workflow as well.
  • Exactly what was committed. Every file in the repository (or folder) is deployed, with its permissions, including folders like bin/ or dist/ that the CLI leaves out of a local folder. Symbolic links are left out.
  • Limits. Up to 64 MB to download, 256 MB unpacked and 20,000 files. Bigger than that, deploy just the app's folder, or use the CLI.
  • A Deploy button for your README. Link to https://velixir.net/new/github?repo=https://github.com/you/app (the button image is https://velixir.net/img/deploy-on-velixir.svg) and anyone can deploy their own copy of your public repository in a few clicks.

Private repositories can't be fetched from a link, and velixir never asks for access to your GitHub account. Deploy them with the CLI from your machine, or with GitHub Actions on every push: both upload the code without velixir reading anything on GitHub. (GitHub answers "not found" for a private repository and a missing one alike, so if a public link says it can't be opened, check it for a typo.)

Via GitHub Actions

A GitHub Actions workflow can deploy every push for you. It installs the velixir CLI on the runner and runs velixir deploy, the same command you run on your own machine. Your source is built on velixir, not on the runner, so the workflow needs no Docker, no language toolchain, and no GitHub App or webhook.

The quickest route is your app's Deploys tab: it has this workflow with the app's ID already filled in, ready to copy, for deploying on every push, on each published release, or only when you run it by hand. The steps below set up the same thing, with the app ID kept in a repository variable instead.

1. Add an API key as a repository secret

Create a key with Write permission under API keys. On GitHub, open your repository's Settings → Secrets and variables → Actions and, on the Secrets tab, add it as VELIXIR_API_KEY.

2. Add your app ID as a repository variable

On the same page, open the Variables tab and add VELIXIR_APP_ID set to your app's ID. velixir apps lists your app IDs, and the app's Deploys tab shows its ID in the velixir deploy --app command. An app ID is not a secret, so a variable is fine.

3. Add the workflow

Save this as .github/workflows/velixir.yml and commit it. If your deploy branch isn't main, change it under branches:.

name: Deploy to velixir

on:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    # One deploy at a time: a push that lands mid-deploy waits for the one before it.
    concurrency:
      group: velixir-deploy
      cancel-in-progress: false
    steps:
      - uses: actions/checkout@v7

      - name: Install the velixir CLI
        shell: bash
        run: |
          curl -fsSL https://velixir.net/install.sh | sh
          echo "$HOME/.velixir/bin" >> "$GITHUB_PATH"

      - name: Deploy
        env:
          VELIXIR_API_KEY: ${{ secrets.VELIXIR_API_KEY }}
          VELIXIR_APP_ID: ${{ vars.VELIXIR_APP_ID }}
        run: |
          if [ -z "$VELIXIR_API_KEY" ] || [ -z "$VELIXIR_APP_ID" ]; then
            echo "::error::Add the VELIXIR_API_KEY secret and the VELIXIR_APP_ID variable under Settings > Secrets and variables > Actions."
            exit 1
          fi
          velixir deploy --app "$VELIXIR_APP_ID"

What each step does:

  • Checkout puts your repository on the runner.
  • Install the velixir CLI downloads the standalone velixir binary (no .NET or other runtime needed) and adds it to the PATH for the next step.
  • Deploy stops with a clear error if the secret or the variable is missing, then runs velixir deploy. That uploads your source, waits while velixir builds and rolls it out, and fails the job if the build or the rollout fails.

Deploys run one at a time: a push that lands while a deploy is running waits for it to finish, so an older commit never replaces a newer one.

4. Push and watch it deploy

Push to your deploy branch. The workflow log follows the deploy through to healthy, and the app's Overview tab shows the new release, then the deployment, then the live badge moving to it.

Variations

  • Stage first. Add --slot staging to the velixir deploy line to deploy without touching live traffic, then swap it live from the Slots tab (staging slots need a Standard plan or above).
  • Deploy one folder of a monorepo. Add --source-dir ./service to upload just that folder.
  • Deploy on release, or by hand. Replace the on: block with release: and types: [published], or with workflow_dispatch: for a Run workflow button. The Deploys tab has all three ready to copy.
  • Don't wait for the rollout. --no-tail ends the job as soon as the deploy is queued. It saves runner minutes, but the job then passes even if the build fails later.
  • Pin the CLI. The install step always fetches the latest CLI. To pin a version, change it to curl -fsSL https://velixir.net/install.sh | VELIXIR_VERSION=2.0.5 sh.

Common gotchas

  • Missing secret or variable. The Deploy step stops with an error that names what to add. Secrets and variables are on separate tabs under Settings → Secrets and variables → Actions.
  • "cannot deploy to it". The API key can see the app but is read-only. Create a key with Write permission and replace the secret.
  • "is not an app ID". VELIXIR_APP_ID must be the app's ID, the long value that looks like 3f2c9a1e-..., not its name or URL.
  • Branch mismatch. The branches: filter decides which pushes deploy. Match it to the branch you deploy from.
  • Build failed. The reason is in the workflow log, and the full build output is on the app's Overview tab. Fix your source and push again.

Slots

Every app has a production slot. Standard plans and above allow more (2 slots in total on Standard, 3 on Pro, up to 10 on Enterprise), so you can deploy a release without touching live traffic:

velixir deploy --slot staging

The staged release answers at staging-<your-app>.velixir.run. When you're happy, open the Slots tab and click Swap slots: the staged release rolls out onto the live slot, with new pods warmed up before they take traffic, and the release that was live moves to the staging slot. Swapping again is your way back. Staging slots run small by default; see Scaling and HA.

Rollback

Every earlier release is listed in the Releases table on the Overview tab. Click Deploy on the one you want and we roll it back out; the edge moves traffic over once it's healthy. Recent Activity on the same tab has a Rollback button for recent deploys, too.

After a deploy

  • Set environment variables on the Environment tab - they're injected on the next deploy.
  • The Logs tab tails container output live.
  • The Metrics strip on the Overview tab updates every 10 seconds with replica count, CPU, memory, and HTTP traffic.