Сначала сделал как описано в этой заметке для Go, но не заработало. Оказалось есть нюансы.

Оказалось, что Rust статически линкуется только с внешними зависимостями на Rust, а с С библиотеками линкуется динамически (libc и т.п.). Проверяется это через утилиту командной строки ldd. Из-за этого собранный бинарник не запускается в контейнерах собранных на базе alpine и scratch. Нужно ставить туда дополнительно libc, libgcc и т.п.

Но есть возможность собрать бинарник в котором все будет слинковано статически. Для этого используется библиотека musl и есть проект muslrust. В данном проекте предлагается собирать бинарник через специальный docker контейнер и нам для Github Actions это не сильно подходит.

Далее есть target x86_64-unknown-linux-musl. При сборке с данным таргетом библиотеки линкуются статически, поэтому его и бдуем использовать в нашем сценарии. Для этого нужно добавить всего два маленьких шага в наш pipeline:

- uses: dtolnay/rust-toolchain@stable
  with:
    targets: x86_64-unknown-linux-musl

- uses: taiki-e/setup-cross-toolchain-action@v1
  with:
    # NB: sets CARGO_BUILD_TARGET evar - do not need --target flag in build
    target: x86_64-unknown-linux-musl

После этого бинарник успешно запускается в scratch образе.

И вторая проблема связана с багом docker-compose. Если контейнер запущен и при docker-compose up происходит recreate, то контейнер автоматически не стартует. Данная проблема описана уже давно и без решения (возможно в одном из обновлений исправят). Поэтому в секции deploy вызываем два раза docker-compose:

- name: Run container
  run: |
    cd ~
    docker-compose up -d --build --pull=always
    docker-compose start &

Так же добавляем кэш зависимостей с помощью actions/cache

- name: Set up cargo cache
  uses: actions/cache@v4
  with:
    path: |
      ~/.cargo/bin/
      ~/.cargo/registry/index/
      ~/.cargo/registry/cache/
      ~/.cargo/git/db/
      target/            
    key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
    restore-keys: ${{ runner.os }}-cargo-

Ну и ниже полный файл сценария (где myapp – имя бинарника)

name: Rust

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

env:
  CARGO_TERM_COLOR: always
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
      with:
        targets: x86_64-unknown-linux-musl

    - uses: taiki-e/setup-cross-toolchain-action@v1
      with:
        # NB: sets CARGO_BUILD_TARGET evar - do not need --target flag in build
        target: x86_64-unknown-linux-musl

    - name: Set up cargo cache
      uses: actions/cache@v4
      with:
        path: |
          ~/.cargo/bin/
          ~/.cargo/registry/index/
          ~/.cargo/registry/cache/
          ~/.cargo/git/db/
          target/            
        key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: ${{ runner.os }}-cargo-
        
    - name: Build
      run: cargo build --release
    - name: Test
      run: cargo test --release
    - name: Copy binary
      run: cp ./target/x86_64-unknown-linux-musl/release/myapp ./  
    - name: upload artifact
      uses: actions/upload-artifact@v4
      with:
        name: my-artifact
        path: |
          Dockerfile.action
          myapp
            
  build-and-push-image:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Update cert
        run: |
          sudo update-ca-certificates
          sudo cp /etc/ssl/certs/ca-certificates.crt ca-certificates.crt
          mkdir zoneinfo
          sudo cp -r /usr/share/zoneinfo/* ./zoneinfo

      - name: Log in to the Container registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - uses: actions/download-artifact@v4
        with:
          name: my-artifact

      - name: set permissions
        run: chmod a+x myapp

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          file: Dockerfile.action
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    runs-on: self-hosted
    needs: build-and-push-image

    steps:
      - name: Log in to the Container registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Run container
        run: |
          cd ~
          docker-compose up -d --build --pull=always
          docker-compose start &

Файл Dockerfile.action берем из ранее указанной статьи про Go