.NET Application Linux SSH Deployment Using Gitlab CI/CD & Github Actions
Setup
- Create a .NET 8 application, call it anything you like (
CiDemoin my example):md CiDemo && cd CiDemo dotnet new sln dotnet new web -o src/Api dotnet sln add .\src\Api\Api.csproj - Ensure .NET 8 runtime is installed on the VM.
- Create a systemd service on the VM
sudo nano /etc/systemd/system/demo-app.service, and paste the following:[Unit] Description=Example .NET Web API App running on Linux [Service] WorkingDirectory=/var/www/demo-app ExecStart=/usr/bin/dotnet /var/www/demo-app/Api.dll Restart=always # Restart service after 10 seconds if the dotnet service crashes: RestartSec=10 KillSignal=SIGINT SyslogIdentifier=dotnet-example User=www-data Environment=ASPNETCORE_ENVIRONMENT=Development Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false Environment=ASPNETCORE_URLS=http://localhost:5001 [Install] WantedBy=multi-user.target - Enable the Service:
sudo systemctl enable demo-app.service - At this point you can install whatever reverse proxy that suits you (nginx, apache, caddy etc..), and configure it to proxy
requests to
http://localhost:5001, which we specified in the systemd service environment variables. - Create the Build Folder: Run
md /var/www/demo-app
SSH Setup
- Generate a New Key Pair on Your Local Machine(linux/mac/wsl preferred here for later purposes): You can use
ssh-keygen -t rsa -b 4096 -C "[email protected]"(no passphrase) - Transfer Public Key to VM: Copy the public key (id_rsa.pub or similar) to your VM and append it to the ~/.ssh/authorized_keys file. Ensure you're copying the public key, not the private one. You can do this using ssh-copy-id if it's available:
ssh-copy-id -i /path/to/public/key username@vm_ip_address, or, manually copy the content of your public key file and paste it into the authorized_keys file on the VM. - Test SSH Connection: Using your newly generated private key, try to SSH into your VM:
ssh -i /path/to/new/private/key username@vm_ip_address - Ensure Correct Permissions: On the VM, make sure the ~/.ssh directory has 700 permissions and the ~/.ssh/authorized_keys file has 600 permissions. You can set these permissions with the following commands:
chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys - Configure Sudo Permissions on the Target Machine: Ensure the user (let's call them ci-user) used for SSH in your CI/CD pipeline has the necessary sudo privileges. You have two main options:
- Passwordless Sudo for Specific Commands: This is more secure than giving blanket passwordless sudo access. Edit the sudoers file using
sudo visudoand add a line like:ci-user ALL=(ALL) NOPASSWD: /path/to/specific/command. Replace /path/to/specific/command with the command you want to allow. This allows ci-user to run this command with sudo without being prompted for a password. - Passwordless Sudo for All Commands: This is less secure and should be used cautiously. Add the following line to give ci-user passwordless sudo access for all commands:
ci-user ALL=(ALL) NOPASSWD: ALL
- Passwordless Sudo for Specific Commands: This is more secure than giving blanket passwordless sudo access. Edit the sudoers file using
- Base64 Encode Your Private Key: Either encode the private key file directly to a
.txtfile(preferred):cat ~/.ssh/my_key | base64 -w0 > ~/encoded_key.txt, or, copy the content of your private key (the one the doesn't end with.pub) and execute the followingcat PRIVATE_KEY_CONTENT | base64 -w0 > ~/encoded_key.txt - Next: Choose your CI/CD tool.
Github Actions:
Copy the contents of
~/encoded_key.txtGo to
Your repo > Settings > Secrets and Variables > Actions > New repository secret.Give it a name, eg:
DEPLOY_PRIVATE_KEY, and paste the content of~/encoded_key.txtto the "Secret" text area.note: you should also create secrets and variable for your VM IP address and ssh username, which we named "ci-user" earlier in the sudo permissions step, but I'll keep it simple for now.
In the root directory of your code, create the following folders:
.github/workflowsInside the
.github/workflowsfolder create a YAML file, call itdeploy.ymlfor example.Paste the following into the YAML file:
name: CI/CD Pipeline
on: push: branches: - dev paths: - src/**
jobs: build: runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: "Setup .NET"
uses: actions/setup-dotnet@v3
with:
dotnet-version: "8.0.x"
- name: Build and Publish
run: |
dotnet restore ApiDemo.sln
dotnet publish src/Api/Api.csproj --no-restore --configuration Release -o published
- name: Setup SSH and Deploy
shell: bash
run: |
apt-get update -y && apt-get install openssh-client -y
mkdir dist
echo "hi there" > dist/index.html
mkdir -p ~/.ssh
eval "$(ssh-agent -s)"
echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
echo "${{ secrets.DEPLOY_PRIVATE_KEY }}" | base64 -d > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-add ~/.ssh/id_rsa
ssh-keyscan vm_ip_address >> ~/.ssh/known_hosts
ssh -o StrictHostKeyChecking=no username@vm_ip_address "if [ -d /var/www/demo-app ]; then rm -rf /var/www/demo-app; fi"
ssh -o StrictHostKeyChecking=no username@vm_ip_address "mkdir /var/www/demo-app"
scp -r -o StrictHostKeyChecking=no published/* username@vm_ip_address:/var/www/demo-app
ssh -o StrictHostKeyChecking=no username@vm_ip_address "sudo systemctl restart demo-app.service"
echo 'deployment complete'
```
- Go to your code, checkout the dev branch (since this will only run on the dev branch as specified in the beginning), and push to github, and voila.
Gitlab CI/CD
Copy the contents of
~/encoded_key.txtGo to
Your repo > Settings > CI/CD > Variables > Expand > Add Variable.Give it a key, eg:
DEPLOY_PRIVATE_KEY, and paste the content of~/encoded_key.txtto the "Value" text area.note: you should also create secrets and variable for your VM IP address and ssh username, which we named "ci-user" earlier in the sudo permissions step, but I'll keep it simple for now.
In the root directory of your code, create the following file:
.gitlab-ci.ymlPaste the following into the
.gitlab-ci.ymlfile:stages: - build - deploy build_job: stage: build image: mcr.microsoft.com/dotnet/sdk:8.0 script: - dotnet restore - dotnet build --no-restore - dotnet publish --no-restore --configuration Release -o published artifacts: paths: - published/ expire_in: 1 hour deploy_job: stage: deploy image: ubuntu:latest before_script: - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' - mkdir -p ~/.ssh - eval $(ssh-agent -s) - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' script: - ssh-add <(echo "$DEPLOY_PRIVATE_KEY" | base64 -d) - ssh -o StrictHostKeyChecking=no ci-user@vm_ip_address "if [ -d /var/www/demo-app ]; then rm -rf /var/www/demo-app; fi" - ssh -o StrictHostKeyChecking=no ci-user@vm_ip_address "mkdir /var/www/demo-app" - scp -r -o StrictHostKeyChecking=no published/* ci-user@vm_ip_address:/var/www/demo-app - ssh -o StrictHostKeyChecking=no ci-user@vm_ip_address "sudo systemctl restart demo-app.service" only: - dev- Go to your code, checkout the dev branch (since this will only run on the dev branch as specified in the beginning), and push to gitlab, and voila.
