Back to Blog

Simple GitHub Actions + Your Own Server Setup

An easy guide to hooking up GitHub Actions with your self-hosted server without the hassle of self-hosted runners.

Jun 6, 2025

10 min read

github-actionsself-hostingdevops

If you’re anything like me, you love pushing your code to GitHub — it feels clean, organized, done. But when it comes to hosting? You’re not deploying to Vercel, Netlify, or Cloudflare Pages — unless it’s a static site (like this blog, lol). Nope. You’re probably running a sketchy VPS somewhere, maybe even a Raspberry Pi behind a port-forwarded router. But you still want that sweet GitHub Actions integration because pushing code and having it magically deploy is awesome.

Now there are better ways like a self-hosted runner (I talk about that later). Also, I wouldn't say this is very suitable for a team of more than 2-4 people, but for personal projects or small teams, this is a great way to get started in my opinion.

Sometimes you want to be extra and hook directly into your server. Don't @ me.

The Setup

1. Generate a Dedicated SSH Key for GitHub Actions

First, let's get your SSH situation sorted. On your local machine:

ssh-keygen -t rsa -b 4096 -C "Comment to identify this key"

When prompted, save it as something memorable like: /home/username/.ssh/github_actions_key

For the passphrase, you can leave it empty for simplicity, but if you want extra security, set a passphrase (you'll need to add it as a secret later). I do recommend setting one though.

This gives you two files:

  • github_actions_key (private key - keep this secret)
  • github_actions_key.pub (public key - this goes on your server)

2. Set Up SSH Access to Your Server

Copy the public key to your server:

# Copy the public key to your server
ssh-copy-id -i ~/.ssh/github_actions_key.pub user@host

# Or if ssh-copy-id isn't available, do it manually:
cat ~/.ssh/github_actions_key.pub | ssh user@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

Test that it works:

ssh -i ~/.ssh/github_actions_key user@host

3. Create Your Deployment Script

SSH into your server and create a deployment script:

sudo nano /home/username/deploy.sh

A small template for a deployment script:

#!/bin/bash

cd /path/to/your/project
git pull origin main
# Do your deployment stuff here
docker-compose down && docker-compose up -d

Here's my (cursed) deployment script I use for a .NET console app:

#!/bin/bash

PROJECT_DIR="/home/ubuntu/Sushi-v3"
LOG_FILE="$PROJECT_DIR/deploy.log"
echo "$(date): Starting deployment" >> $LOG_FILE

cd "$PROJECT_DIR" || { echo "$(date): Failed to change directory to $PROJECT_DIR" >> $LOG_FILE; exit 1; }

git pull >> $LOG_FILE 2>&1

if [ $? -eq 0 ]; then
    echo "$(date): Git pull successful" >> $LOG_FILE

    echo "$(date): Stopping existing app" >> $LOG_FILE
    screen -S sushi -X quit 2>/dev/null || true

    echo "$(date): Building .NET application" >> $LOG_FILE
    dotnet build Sushi.csproj --configuration Release >> $LOG_FILE 2>&1

    if [ $? -eq 0 ]; then
        echo "$(date): Build successful, starting application" >> $LOG_FILE

        BUILD_PATH="$PROJECT_DIR/bin/Release/net6.0"

        if [ ! -f "$BUILD_PATH/Sushi" ]; then
            echo "$(date): ERROR - Executable not found at $BUILD_PATH" >> $LOG_FILE
            exit 1
        fi

        screen -dmS sushi bash -c "cd $BUILD_PATH && ./Sushi"

        sleep 3

        if screen -list | grep -q "sushi"; then
            echo "$(date): Application started successfully in screen session 'app'" >> $LOG_FILE
        else
            echo "$(date): Application failed to start" >> $LOG_FILE
            exit 1
        fi
    else
        echo "$(date): Build failed" >> $LOG_FILE
        exit 1
    fi

    echo "$(date): Deployment completed successfully" >> $LOG_FILE
else
    echo "$(date): Git pull failed" >> $LOG_FILE
    exit 1
fi

Main components are just pulling the latest code from your repo, building it, and starting it in a screen session or whatever you use.

Make it executable:

chmod +x /home/username/deploy.sh

Test it manually:

./deploy.sh

4. Set Up GitHub Repository Secrets

In your GitHub repo, go to Settings → Secrets and variables → Actions → New repository secret.

Add these four secrets:

HOST: Your server's public IP address or domain.

USERNAME: Your SSH username.

SSH_KEY: Your private key content

# Copy the private key content
cat ~/.ssh/github_actions_key

Copy the entire output (including -----BEGIN OPENSSH PRIVATE KEY----- and -----END OPENSSH PRIVATE KEY-----) and paste it as the SSH_KEY secret.

SSH_PASSPHRASE: If you set a passphrase when generating your SSH key, add it here. If not, you can skip this.

5. Create the GitHub Actions Workflow

In your repo, create .github/workflows/deploy.yml:

name: Deploy to My Server

on:
    push:
        branches: [main]

jobs:
    deploy:
        runs-on: ubuntu-latest

        steps:
            - name: Deploy via SSH
              uses: appleboy/ssh-action@v1
              with:
                  host: ${{ secrets.HOST }}
                  username: ${{ secrets.USERNAME }}
                  key: ${{ secrets.SSH_KEY }}
                  passphrase: ${{ secrets.SSH_PASSPHRASE }} # Optional, if you set a passphrase on your SSH key
                  script: |
                      /home/${{ secrets.USERNAME }}/deploy.sh

6. Test Your Setup

Push something to your main branch and watch the Actions tab in GitHub. You should see your workflow run, and if everything's set up right, your server should update automatically.

Check the deployment log on your server:

tail -f /var/log/deploy.log

Troubleshooting (Because Nothing Ever Works the First Time)

Permission Denied on SSH: Make sure your private key permissions are restrictive and the public key is correctly added to ~/.ssh/authorized_keys on your server.

chmod 600 ~/.ssh/github_actions_key
chmod 700 ~/.ssh

# Ensure your public key is in ~/.ssh/authorized_keys on the server
# Identify it using the comment you added when generating the key
ssh -i ~/.ssh/github_actions_key user@host "cat ~/.ssh/authorized_keys"

Git Authentication Issues: If your repo is private, you'll need to set up SSH access for git too:

# On your server, generate another key for git
ssh-keygen -t rsa -b 4096 -C "server-git-access"
# Add the public key to your GitHub account under SSH keys

Deploy Script Fails: Check the log file.

Workflow Doesn't Trigger: Double-check your branch name in the workflow file. Is it main or master? GitHub's been switching defaults.

Pro Tips (aka Lessons from My Mistakes)

Use Absolute Paths: Your deployment script might run from a different directory than you expect. Always use full paths like /home/username/project instead of ~/project.

Test the SSH Action Locally: You can simulate what GitHub Actions does:

ssh -i ~/.ssh/github_actions_key user@host "/home/username/deploy.sh"

GitHub Actions Logs: If your workflow fails, check the logs in the Actions tab. They can be super helpful for debugging.

Set Up Health Checks: Add a simple check to verify your app is running. I did that using screen:

# At the end of your deploy script
if screen -list | grep -q "sushi"; then
    echo "$(date): App is running in screen session" >> $LOG_FILE
else
    echo "$(date): App not found in screen sessions" >> $LOG_FILE
fi

Using systemd: If you're running a service, consider using systemd to manage it. It can restart your app if it crashes and gives you better logging. screen or tmux are usable for quick and dirty setups, but systemd is more robust. Although, it is a bit more complex to set up but the benefits are worth it.

Consider Using a Deploy Key: Create a deploy key specifically for your GitHub repo. This way, you can keep your personal SSH keys separate from deployment tasks.

More:

  • Backup your ssh keys — GitHub Secrets don't give them back.
  • Use pull_request events if you want to preview before deploying.
  • Throw in uptime / df -h / htop commands to check your server mid-deployment.

Why Not Just Use a Self-Hosted Runner?

Valid question. Self-hosted runners are probably the "right" way to do this. They're more secure, give you better control, and integrate seamlessly with Actions.

But:

  • They require more setup.
  • More resource usage.
  • They can be overkill for small projects or personal use.

Sometimes you just want something simple that works without setting up another service. This approach is basically using GitHub Actions as a fancy webhook system, and honestly? It works fine for small projects.

If you want to learn more about self-hosted runners, check out the GitHub docs.

Reality Check

This setup works great for personal projects and small teams. If you're doing anything serious with compliance requirements or multiple environments, please use proper CI/CD tools. Your DevOps team will thank you.

Also remember - your server needs to be accessible from the internet for this to work. If you're behind NAT or a restrictive firewall, you'll need to sort that out first.

Wrapping Up

That’s the gist.

You push to main, GitHub deploys your code to your janky but lovable server. You didn’t pay for fancy CI/CD. You didn’t cry setting up a cloud pipeline. You just… automated your life a bit.

Clean. Controlled. Kinda cursed (just a little).

© 2025 Arikatsu