Deploy Rust Axum Binary with `garust-debian` GitHub Action

Seto Elkahfi
SmbPndk
Published in
5 min readMay 13, 2023

Rust Axum has recently been my defacto option for developing REST API endpoints. Although I have not moved on from Rails entirely, it’s been a pleasant experience. With Rust compilation and strong type, great tooling, and a great community, I’m increasingly familiar with how things work together.

I recently needed to do a deployment to a Debian server for my toy project called SplitFire. It’s fairly straightforward, but I want to document my journey up until how I ended up creating my own GitHub action for it. I’ll refer to GitHub action as GA for the rest of the article. Follow along!

The Classic push-to-deploy

The first step in my journey is the classic push-to-deploy. I document this journey in this article. Though it’s based on a Rails app, the principle stays the same. Here’s the video of me doing it.

In-server Build Command

In this first iteration, I ran the build command in the server itself. After creating a git — bare repository, I simply call the cargo release command. I’ll explain this step in detail in another article.


$ cargo run --release > /dev/null 2>&1 &

But here’s the video of me doing it.

This setup requires me to set up the Rust toolchain in the Debian server. It feels cumbersome for me since I don’t do development on the server, obviously. So, the toolchain is only being used during the deployment, that is when building the project for release. There must be a better approach.

GitHub Action build command

It does. This is the last iteration before I finally created the garust-debian GA. The idea is to outsource the build for the release command in a GA runner. This GA will take several inputs that will be explained in each step:

inputs:
project-directory:
description: "Working directory for the build."
required: true
project-name:
description: "Name of the binary to run."
required: true
ssh-user:
description: "SSH user."
required: true
ssh-host:
description: "SSH host."
required: true
ssh-private-key:
description: "SSH private key."
required: true
ssh-known-hosts:
description: "SSH known hosts."
required: true

Rust Toolchain

In the first step, I set up a GitHub action with the dtolnay toolchain. This has become the standard in the Rust community replacing the unmaintained action-rs.

    - name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Build for release
run: cargo build --release
shell: bash

SSH on the GA runner

The main idea is to use the GA runner to ssh and rsync the target folder to the server. So, the next step is to set up the GA to be able to ssh to the Debian server. For this task, I use ssh-key-action.

    - name: Install SSH key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ inputs.ssh-private-key }}
known_hosts: ${{ inputs.ssh-known-hosts }}

Public/Private Keys

The step requires me to set up the SSH key and put it in the repo’s secret. The following step is best to do on the server. But arguably, it can be done in one’s local machine as well.

$ // Create a new ssh public/private key pair in the .ssh directory
$ cd
$ ssh-keygen -t rsa -b 4096 -C "info@splitfire.ai"
Generating public/private rsa key pair.
Enter file in which to save the key (/home/deploy/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/deploy/.ssh/id_rsa
Your public key has been saved in /home/deploy/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:NQrYsdgHROcMBg9wdf32Cu3WH04rU6FUIrgvudfb1AjU info@splitfire.ai
The key's randomart image is:
+---[RSA 4096]----+
| o+O=* . |
| . Xo@ = o |
| o O * B |
| . oE+ = + |
| ..S.+ = + |
| .. = . = |
| o.o . o |
| o ... o |
| . .. . |
+----[SHA256]-----+

I named the file ga-debian. That command will produce ga-debian.pub (the public key) and the ga-debian (the private key) keys. If one doesn’t specify the name is id_rsa.

Authorized Keys and Unknown Hosts

The next step is to add the public key to the authorized keys. Still in the .ssh folder:

$ cat ga-debian.pub >> ~/.ssh/authorized_keys

The next step is to set up a secret in the GitHub repo. It’s in the Settings -> Secrets and variables -> Actions. I name it ssh-private-key. Copy-paste the content of the private key here. The easiest way is to open the private key file and then just copy pasted it :D.

The next step is to update the known_hosts value in the GA runner. This can be obtained by looking at the known_hosts in your local machine, assuming you’re able to ssh to your server :D. I name it ssh-known-hosts in the repo’s secrets settings.

Sync Target Folder

The next step is to rsync the /target/release directory to the folder where you put the binary in the server. This step takes three inputs: the ssh user (ssh-user input), the ssh host (ssh-host input), and the project directory path relative to the ssh user home folder in the server (project-directory input).

    - name: rsync over SSH
run: rsync -r ./target/release/ ${{ inputs.ssh-user }}@${{ inputs.ssh-host }}:${{ inputs.project-directory }}
shell: bash

A Simple Bash

The last step is to run the binary in the server, from the GA runner. For this, I need a simple bash script. This step requires the ssh-user and ssh-host inputs that were used in the previous step.

    - name: Start process
run: ssh ${{ inputs.ssh-user }}@${{ inputs.ssh-host }} 'bash -s' < start.sh ./${{ inputs.project-directory }} ${{ inputs.project-name }}
shell: bash
#!/bin/bash

echo "------------------------ STARTING DEPLOYMENT ------------------------"

# Check if the first argument is a directory
if [ ! -d "$1" ]
then
echo "😩 $1 is not a directory."
echo "🛑 Deployment failed."
exit 1
fi

# Change to working directory
cd $1

# Check if process is running

# Check if the second argument is a process
if [ -z "$2" ]
then
echo "😩 No process name provided."
echo "🛑 Deployment failed."
exit 1
fi
process_name=$2

# Check if binary exists
if [ ! -f "$process_name" ]
then
echo "😩 $process_name does not exist."
echo "🛑 Deployment failed."
exit 1
fi

echo "✅ Arguments are valid."

# Check if process is running
echo "🔍 Looking for $process_name process."
PID=$(pidof $process_name)

echo "🤨 Killing or not killing $process_name."

if [ ! -z "$PID" ]
then
echo "✅ $process_name is running as $PID."
echo "⏳ Killing it."
kill -9 $PID
echo "✅ Killed $process_name."
else
echo "$process_name is not running."
echo "No killing needed."
fi

echo "🚀 Starting $process_name."

./$process_name > /dev/null 2>&1 &

echo "------------------------ DEPLOYMENT COMPLETED ------------------------"

exit 0

Conclusion

There I have it. A fully functional deployment pipeline for Rust Axum REST API endpoints project based on GitHub Action workflow. With this repo, I don’t need to install the Rust toolchain to only use it during the deployment, like when I use the push-to-deploy method. I hope this article has been useful for you as it’s been for me.

You can check the repo here.

--

--

Seto Elkahfi
SmbPndk
Editor for

Software developer. Rust, Ruby on Rails, AWS, iOS, tvOS,