CI/CD for .NET with GitHub Actions: Build, Test and Deploy Automatically
A practical GitHub Actions pipeline for ASP.NET Core: restore, build, test, publish, and deploy safely with secrets and environment approvals.

Manual deployments are slow and risky. Someone builds on their own machine, copies files to a server and hopes nothing was missed. A CI/CD pipeline makes every change go through the same steps: build, test, package and deploy. GitHub Actions is a popular choice for .NET teams because it lives next to the code and is easy to start with.
The goal
- Every push and pull request is built and tested automatically.
- Merges to the main branch produce a versioned artifact.
- Deployments to production happen from that artifact, with approval and secrets kept safe.
A basic CI workflow
Create .github/workflows/ci.yml:
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
cache: true
cache-dependency-path: "**/packages.lock.json"
- run: dotnet restore --locked-mode
- run: dotnet build --no-restore -c Release
- run: dotnet test --no-build -c Release --logger trx --results-directory TestResults
- run: dotnet publish src/Shop.Api -c Release -o publish --no-build
- uses: actions/upload-artifact@v4
with:
name: api
path: publish
A few choices worth explaining:
--locked-modewith apackages.lock.jsonmakes restores reproducible.--no-restoreand--no-buildavoid repeating work in later steps.- Artifacts let the deploy job use exactly what was tested.
Running tests that need a database
Integration tests often need SQL Server or PostgreSQL. GitHub Actions supports service containers:
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports: ["5432:5432"]
Your tests connect to localhost:5432. Alternatively, use Testcontainers inside the tests themselves.
Deploying with environments and approvals
Add a deploy job that runs only on the main branch and uses a GitHub environment:
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/download-artifact@v4
with:
name: api
path: publish
- name: Deploy
run: ./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
In repository settings, configure the production environment with required reviewers. The job then waits for an approved person before deploying. Secrets defined on the environment are only available to jobs that use it.
Where to deploy
- Azure App Service: use the official Azure login and web app deploy actions, preferably with OpenID Connect instead of long-lived passwords.
- Containers: build and push a Docker image to a registry, then update your container service or Kubernetes deployment.
- Your own Windows or Linux server: use a self-hosted runner inside your network or a secure deployment script over SSH.
Database migrations
Decide how schema changes reach production. Common options:
- Generate an idempotent SQL script with
dotnet ef migrations script --idempotentand apply it as a deploy step. - Use EF Core migration bundles (
dotnet ef migrations bundle) for a single executable that applies migrations.
Avoid applying migrations automatically at application startup in production for anything beyond small apps, because failures become harder to control.
Good practices
| Practice | Why |
|---|---|
| Require CI to pass before merging | Broken code never reaches main |
| Keep secrets in GitHub secrets or OIDC | No passwords in the repository |
| Version artifacts with the commit SHA | Easy rollback and traceability |
| Use environment approvals for production | A human confirms risky changes |
| Cache NuGet packages | Faster pipelines |
| Run security scanning | Catch vulnerable dependencies early |
Frequently asked questions
GitHub Actions or GitLab CI? Both work very well for .NET. Use the platform where your code already lives. The concepts are the same: build, test, artifact, and protected deployment.
How long should a pipeline take? Aim for under ten minutes for pull request checks so developers get fast feedback. Use caching, run slow tests separately, and parallelise where possible.
Can we deploy to an on-premise IIS server? Yes. A self-hosted runner inside your network can stop the site or app pool, copy the published files and start it again, or you can use Web Deploy. Keep credentials in the CI secret store.
How do we roll back a bad release? Redeploy the previous artifact, which is easy when every build is versioned and stored. For database changes, write migrations that are backward compatible so the previous version can still run.
Key takeaways
- Build and test every push and pull request, and publish one artifact per change.
- Deploy that same artifact using protected environments with approvals and scoped secrets.
- Plan database migrations as an explicit deploy step.
- Start simple, then add caching, scanning and container builds as the project grows.
KHR Digital Solutions sets up CI/CD pipelines on GitHub and GitLab for .NET and Next.js projects, including deployments to cloud and on-premise servers.
Related articles

Entity Framework Core Performance: 10 Practical Tips for Faster .NET Apps
Slow pages are often slow queries. Ten EF Core techniques, from AsNoTracking and projections to split queries and compiled models, with code examples.

Docker for .NET Developers: From First Container to Production Image
A practical walkthrough of containerising an ASP.NET Core app: multi-stage builds, small secure images, configuration, health checks and common mistakes.

ASP.NET Core Minimal APIs vs Controllers: An Architect's Guide for 2026
Beyond hello-world: how minimal APIs and controllers compare on structure, validation, filters, OpenAPI, versioning, testing and performance in large production codebases.