Step-by-Step: Setting Up GitHub Actions to Build and Deploy .NET to EC2

Let's learn how to set up a GitHub Actions build-deploy pipeline to deploy an ASP.NET API application to an Amazon EC2 instance, running as a systemd service.

Rahul Pulikkot Nath
Rahul Pulikkot Nath

Table of Contents

In this post, we’ll set up a build and deployment pipeline using GitHub Actions to deploy an ASP.NET Core application to an AWS EC2 instance.

We’ll configure the GitHub Actions workflow to:

  • Build the ASP.NET Core app
  • Connect to the EC2 instance
  • Deploy the app as a systemd service so it runs in the background

This post is part of my AWS EC2 series, and thanks to AWS for sponsoring this video.

If you are completely new to running .NET apps on AWS EC2 I highly recommend checking out my Step-by-Step guide.

Deploying a .NET Web API on Amazon EC2: A Step-by-Step Guide
Let’s learn how to leverage Amazon EC2 to host your .NET applications. In this post, we will learn how to create an EC2 instance, set it up with .NET runtime, upload your .NET application, and run it from there.

Setting Up EC2 Instance

Before we dive into the GitHub Actions setup, we need an EC2 instance ready to host our ASP NET Core app.

We will use AWS CDK to define and deploy the required AWS infrastructure.

I will be running the CDK deploy step manually outside of GitHub Actions to keep our build and deployment pipeline faster and more focused on the app itself.

AWS CDK For The .NET Developer: How To Easily Get Started
AWS CDK simplifies Cloud Infrastructure management. CDK lets you define AWS resources using first-class programming concepts. It translates your code into AWS CloudFormation templates, which can be used to provision AWS resources for your application. Let’s get started using .NET and C#.

Here's a snippet from the CDK code to set up the EC2 instance.

// Create EC2 InstanceMore actions
var instance = new Instance_(this, "WebApiInstance", new InstanceProps {
  InstanceType = InstanceType.Of(InstanceClass.BURSTABLE3, InstanceSize.MICRO),
    MachineImage = MachineImage.LatestAmazonLinux2023(),
    Vpc = vpc,
    Role = role,
    SecurityGroup = securityGroup,
    KeyPair = KeyPair.FromKeyPairName(this, "key-0a498c763ef8ab954", "my-key-pair"),
});

To set up the key pair navigate to the AWS Console → EC2 → Key Pairs

We use this key pair to connect to the EC2 instance later from GitHub actions file to deploy our .NET application.

To keep our GH Actions file more focused on the application, we will be running the infrastructure setup outside of it. For this example, you can directly run the cdk deploy command from the root of the repository, which will create the EC2 instances.

Alternatively, you can also have a separate GH actions or include it in your application deployment steps as well.

Deploying ASP.NET App as Systems Service File

For this demo, we will use a simple ASP.NET Core Web Api application created by the default template.

We’ll run the app in the background using a systemd service.

💡
A systemd service lets you manage your application like any other Linux service — you can start, stop, restart, and enable it to run automatically on boot.

Here is the systemd service file we will use.

Key Sections of systemd File

  • [Unit]
    • Description: Provides a description of the service.
  • [Service]
    • WorkingDirectory: The directory where your app is located.
    • ExecStart: The command to start your application.
    • Restart=always: Ensures the app restarts automatically if it crashes or stops.
    • User: Specifies the user under which the app runs (ec2-user).
    • Environment: Sets environment variables:
      • DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1: Enables globalization features without needing ICU libraries.
      • ASPNETCORE_ENVIRONMENT=Production: Sets the environment to Production.
      • ASPNETCORE_URLS=http://0.0.0.0:5000: Binds the app to all network interfaces on port 5000.
  • [Install]
    • WantedBy=multi-user.target: Ensures the service starts automatically on system boot.

GitHub Actions Build and Deployment Pipeline

With that all out of the way, let's set up our GitHub Actions pipeline to deploy our ASP.NET app to the EC2 instance.

Whenever code is pushed to the branch, the workflow:

  • Builds the application into a self-contained, production-ready artifact.
  • Deploys it to an EC2 instance, stopping the current service, copying the new build, and restarting the service.

Building the .NET Publish Artifact

This GitHub Actions workflow starts when a push is made to the test branch. It prepares the environment and builds the application for deployment:

  • Uses .NET 9.0.x, configured via actions/setup-dotnet.
  • Check out the code from the repository.
  • Runs dotnet publish from the src/ directory, targeting linux-x64 and generating a self-contained release build.
  • Outputs the build to a ./publish folder for deployment.
- uses: actions/checkout@v3

- name: Setup .NET
  uses: actions/setup-dotnet@v3
  with:
    dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Publish
  working-directory: src
  run: dotnet publish ${{ env.PROJECT_PATH }} --configuration Release --output ./publish --self-contained true -r linux-x64

This ensures a clean, ready-to-run build that includes the .NET runtime.

Deploying to EC2

After the build step, the workflow deploys the application to an EC2 instance using a secure SSH connection. It authenticates using AWS credentials from GitHub Secrets, then handles the deployment process end-to-end.

Key steps include:

  • Connecting via SSH with a private key provided at runtime
  • Stopping the existing service and cleaning the deployment directory
  • Transferring the published files and service definition to the EC2 instance
  • Reloading systemd and restarting the service to run the new version
- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v2
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: ${{ secrets.AWS_REGION }}

- name: Copy files to EC2
  working-directory: src
  env:
    EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
    EC2_USERNAME: ${{ secrets.EC2_USERNAME }}
  run: |
    echo "$EC2_SSH_KEY" > ssh_key.pem
    chmod 600 ssh_key.pem

    # Stop the service and clear the directory
    ssh -o StrictHostKeyChecking=no -i ssh_key.pem $EC2_USERNAME '
     sudo systemctl stop myapi || true
     rm -rf ~/myapi/*
    '

    # Copy the published files
    scp -o StrictHostKeyChecking=no -i ssh_key.pem -r ./publish/* $EC2_USERNAME:~/myapi/

    # Copy the service file
    scp -o StrictHostKeyChecking=no -i ssh_key.pem ./MyApi/myapi.service $EC2_USERNAME:~/myapi/

    # Setup and start the service
    ssh -o StrictHostKeyChecking=no -i ssh_key.pem $EC2_USERNAME '
     sudo cp ~/myapi/myapi.service /etc/systemd/system/
     sudo chmod +x ~/myapi/MyApi
     sudo systemctl daemon-reload
     sudo systemctl enable myapi
     sudo systemctl restart myapi
    '

This ensures a consistent and automated deployment with minimal manual intervention.

The GH Actions will be automatically triggered on commits in the main branch, or you can trigger a manual build under the Actions tab.

Once deployed, you can navigate to your ASP.NET API via the EC2 instance's public DNS address and the port (5000) on which the application is configured.

You can view the code at this commit, which captures the project as of that date. For the latest version, please check the main branch.

AWSEC2