Deep-dive on the Next Gen Platform. Join the Webinar!

Skip Navigation
Show nav
Dev Center
  • Get Started
  • Documentation
  • Changelog
  • Search
  • Get Started
    • Node.js
    • Ruby on Rails
    • Ruby
    • Python
    • Java
    • PHP
    • Go
    • Scala
    • Clojure
    • .NET
  • Documentation
  • Changelog
  • More
    Additional Resources
    • Home
    • Elements
    • Products
    • Pricing
    • Careers
    • Help
    • Status
    • Events
    • Podcasts
    • Compliance Center
    Heroku Blog

    Heroku Blog

    Find out what's new with Heroku on our blog.

    Visit Blog
  • Log inorSign up

Getting Started on Heroku with .NET

Introduction

Complete this tutorial to deploy a sample .NET app to Cedar, the legacy generation of the Heroku platform. To deploy the app to the Fir generation, only available to Heroku Private Spaces, follow this guide instead.

The tutorial assumes that you have:

  • A verified Heroku Account
  • An Eco dynos plan subscription (recommended)
  • .NET SDK 9.0+ installed locally - you can download the latest “Build apps - SDK” installer for your OS and architecture on the .NET 9.0 download page.

Using dynos and databases to complete this tutorial counts towards your usage. We recommend using our low-cost plans to complete this tutorial. Eligible students can apply for platform credits through our new Heroku for GitHub Students program.

Set Up

Install the Heroku Command Line Interface (CLI). Use the CLI to manage and scale your app, provision add-ons, view your logs, and run your app locally.

The Heroku CLI requires Git, the popular version control system. If you don’t already have Git installed, complete the following before proceeding:

  • Git installation
  • First-time Git setup

Download and run the installer for your platform:

apple logomacOS

Install Homebrew and run:

$ brew install heroku/brew/heroku

windows logoWindows

Download the appropriate installer for your Windows installation:

64-bit installer

32-bit installer

You can find more installation options for the Heroku CLI here.

After installation, you can use the heroku command from your command shell.

To log in to the Heroku CLI, use the heroku login command:

$ heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/***
heroku: Waiting for login...
Logging in... done
Logged in as me@example.com

This command opens your web browser to the Heroku login page. If your browser is already logged in to Heroku, click the Log In button on the page.

This authentication is required for the heroku and git commands to work correctly.

If you have any problems installing or using the Heroku CLI, see the main Heroku CLI article for advice and troubleshooting steps.

If you’re behind a firewall that uses a proxy to connect with external HTTP/HTTPS services, set the HTTP_PROXY or HTTPS_PROXY environment variables in your local development environment before running the heroku command.

Clone the Sample App

If you’re new to Heroku, it’s recommended that you complete this tutorial using the Heroku-provided sample app.

If you have an existing app you want to deploy, follow this article instead.

Clone the sample app so that you have a local version of the code. Execute these commands in your local command shell or terminal:

$ git clone https://github.com/heroku/dotnet-getting-started.git
$ cd dotnet-getting-started

You now have a functioning git repository that contains a simple app. It includes a GettingStarted.sln solution file referencing a Frontend.csproj ASP.NET Core project.

Define a Procfile

Use a Procfile, a text file in the root directory of your app, to explicitly declare what command to execute to start your app.

The Procfile in the example app looks like this:

web: cd Frontend/bin/publish/; ./Frontend --urls http://*:$PORT

# Uncomment this `release` process if you are using a database, so that the efbundle
# migrations (compiled after building the application) are run as part of app deployment,
# using Heroku's Release Phase feature:
# https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#efbundle
# https://devcenter.heroku.com/articles/release-phase
# release: Frontend/bin/publish/efbundle

This Procfile declares a single process type, web, and the command needed to run it. The name web is important here because it declares that this process type attaches to Heroku’s HTTP routing stack and receives web traffic when deployed. The command used here runs the published ASP.NET Core web app, and passes in the URL that the app listens on using the PORT environment variable.

Create Your App

Using a dyno and a database to complete this tutorial counts towards your usage. Delete your app, and database as soon as you’re done to control costs.

 

Apps use Eco dynos if you’re subscribed to Eco by default. Otherwise, it defaults to Basic dynos. The Eco dynos plan is shared across all Eco dynos in your account. It’s recommended if you plan on deploying many small apps to Heroku. Learn more here. Eligible students can apply for platform credits through our Heroku for GitHub Students program.

Create an app on Heroku to prepare the platform to receive your source code:

$ heroku create
Creating app...
Creating app... done, whispering-river-68900
https://whispering-river-68900-8c6eb3ace75c.herokuapp.com/ | https://git.heroku.com/whispering-river-68900.git

When you create an app, a Git remote named heroku is also created and added to your local repository configuration. Git remotes are versions of your repository that live on other servers. You can deploy your app by pushing code to that special Heroku-hosted remote associated with your app.

Heroku generates a random name for your app, in this case, whispering-river-68900. You can specify your own app name.

Deploy the App

Using a dyno to complete this tutorial counts towards your usage. Delete your app and database as soon as you’re done to control costs.

Deploy your code. This command pushes the main branch of the sample repo to your heroku remote, which then deploys to Heroku:

$ git push heroku main
remote: Updated 43 paths from 5ef5e31
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Building on the Heroku-24 stack
remote: -----> Determining which buildpack to use for this app
remote: -----> .NET app detected
remote: -----> SDK version detection
remote:        Detected .NET solution: `/tmp/build_28da475e/GettingStarted.sln`
remote:        Inferring version requirement from `/tmp/build_28da475e/GettingStarted.sln`
remote:        Detected version requirement: `^9.0`
remote:        Resolved .NET SDK version `9.0.202` (linux-amd64)
remote: -----> SDK installation
remote:        Downloading SDK from https://download.visualstudio.microsoft.com/download/pr/c2220b38-c512-4447-b564-a18048d14327/965cdfe500a937c2d28bc9d2db45cd1f/dotnet-sdk-9.0.202-linux-x64.tar.gz .... (1.3s)
remote:        Verifying SDK checksum
remote:        Installing SDK
remote: -----> Restore .NET tools
remote:        Tool manifest file detected
remote:        Running `dotnet tool restore --tool-manifest /tmp/build_28da475e/.config/dotnet-tools.json`
remote:
remote:          Tool 'dotnet-ef' (version '8.0.14') was restored. Available commands: dotnet-ef
remote:
remote:          Restore was successful.
remote:
remote:        Done (1.8s)
remote: -----> Publish app
remote:        Running `dotnet publish /tmp/build_28da475e/GettingStarted.sln --runtime linux-x64 "-p:PublishDir=bin/publish" --artifacts-path /tmp/build_artifacts`
remote:
remote:            Determining projects to restore...
remote:            Restored /tmp/build_28da475e/Frontend/Frontend.csproj (in 9.16 sec).
remote:            Frontend -> /tmp/build_artifacts/bin/Frontend/release_linux-x64/Frontend.dll
remote:            Frontend -> /tmp/build_28da475e/Frontend/bin/publish/
remote:            Publishing executable database migration bundle
remote:            Build started...
remote:            Build succeeded.
remote:            Building bundle...
remote:            Done. Migrations Bundle: /tmp/build_28da475e/Frontend/bin/publish/efbundle
remote:            Don't forget to copy appsettings.json alongside your bundle if you need it to apply migrations.
remote:
remote:        Done (24.6s)
remote: -----> Process types
remote:        Detecting process types from published artifacts
remote:        Found `web`: bash -c cd Frontend/bin/publish; ./Frontend --urls http://*:$PORT
remote:        Procfile detected
remote:        Skipping process type registration (add process types to your Procfile as needed)
remote: -----> Done (finished in 31.0s)
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote:
remote: -----> Compressing...
remote:        Done: 117.9M
remote: -----> Launching...
remote:        Released v3
remote:        https://whispering-river-68900-8c6eb3ace75c.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/whispering-river-68900.git
 * [new branch]      main -> main

Visit the app at the URL shown in the logs. As a shortcut, you can also open the website as follows:

$ heroku open

View Logs

Heroku treats logs as streams of time-ordered events, aggregated from the output streams of all your app and Heroku components. Heroku provides a single stream for all events.

View information about your running app by using one of the logging commands, heroku logs --tail:

$ heroku logs --tail
2025-04-04T01:35:45.744471+00:00 app[api]: Initial release by user developer@example.com2025-04-04T01:35:45.744471+00:00 app[api]: Release v1 created by user developer@example.com2025-04-04T01:35:45.934551+00:00 app[api]: Enable Logplex by user developer@example.com2025-04-04T01:35:45.934551+00:00 app[api]: Release v2 created by user developer@example.com2025-04-04T01:35:49.000000+00:00 app[api]: Build started by user developer@example.com2025-04-04T01:36:35.440777+00:00 app[api]: Release v3 created by user developer@example.com2025-04-04T01:36:35.440777+00:00 app[api]: Deploy 6077c4f0 by user developer@example.com2025-04-04T01:36:35.455296+00:00 app[api]: Scaled to web@1:Eco by user developer@example.com2025-04-04T01:36:36.000000+00:00 app[api]: Build succeeded
2025-04-04T01:36:42.071305+00:00 heroku[web.1]: Starting process with command `cd Frontend/bin/publish/; ./Frontend --urls http://*:5908`
2025-04-04T01:36:42.884637+00:00 app[web.1]: warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
2025-04-04T01:36:42.884667+00:00 app[web.1]: No XML encryptor configured. Key {21711fa1-1636-4efc-aba0-5fc2ee2e809f} may be persisted to storage in unencrypted form.
2025-04-04T01:36:42.926692+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[14]
2025-04-04T01:36:42.926694+00:00 app[web.1]: Now listening on: http://[::]:5908
2025-04-04T01:36:42.928041+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[0]
2025-04-04T01:36:42.928042+00:00 app[web.1]: Application started. Press Ctrl+C to shut down.
2025-04-04T01:36:42.928653+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[0]
2025-04-04T01:36:42.928653+00:00 app[web.1]: Hosting environment: Production
2025-04-04T01:36:42.928653+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[0]
2025-04-04T01:36:42.928653+00:00 app[web.1]: Content root path: /app/Frontend/bin/publish
2025-04-04T01:36:43.202406+00:00 heroku[web.1]: State changed from starting to up
2025-04-04T01:36:44.464140+00:00 heroku[router]: at=info method=GET path="/" host=whispering-river-68900-8c6eb3ace75c.herokuapp.com request_id=e768c5fa-3e23-0800-eda4-1ddd83e8d87d fwd="123.456.789.0" dyno=web.1 connect=6002ms service=81ms status=200 bytes=10739 protocol=http1.1 tls=true tls_version=unknown

To generate more log messages, refresh the app in your browser.

To stop streaming the logs, press Control+C.

Push Local Changes

In this step, you deploy a local change to the app to Heroku.

Create a new .NET project using the console template, and add it to the solution file (GettingStarted.sln):

$ dotnet new console -o bgworker
The template "Console App" was created successfully.

Processing post-creation actions...
Restoring ./dotnet-getting-started/bgworker/bgworker.csproj:
  Determining projects to restore...
  Restored ./dotnet-getting-started/bgworker/bgworker.csproj (in 36 ms).
Restore succeeded.
$ dotnet sln add bgworker
Project `bgworker/bgworker.csproj` added to the solution.

Now deploy this local change to Heroku.

Almost every deploy to Heroku follows this same pattern. First, add the modified files to the local git repository:

$ git add .

Commit the changes to the repository:

$ git commit -m "Added bgworker"
[main f6b2236] Added bgworker
 3 files changed, 39 insertions(+), 1 deletion(-)
 create mode 100644 bgworker/Program.cs
 create mode 100644 bgworker/bgworker.csproj

Deploy, just as you did previously:

$ git push heroku main

Start a Console

You can run a command, typically scripts and applications that are part of your app, in a one-off dyno using the heroku run command. You can also run an interactive bash session in your app’s environment:

$ heroku run bash
Running bash on whispering-river-68900...
Running bash on whispering-river-68900... up, run.2093
~ $ dotnet --list-runtimes
Microsoft.AspNetCore.App 9.0.3 [/app/.heroku/cnb/dotnet/layers/runtime/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 9.0.3 [/app/.heroku/cnb/dotnet/layers/runtime/shared/Microsoft.NETCore.App]
~ $ ls -C
Frontend        LICENSE.txt  README.md  docker-compose.yml
GettingStarted.sln  Procfile     app.json
~ $ exit

If you receive an error, Error connecting to process, configure your firewall.

When the bash shell is ready, you can run commands in the same environment as your dyno. For example, dotnet --list-runtimes to see the installed runtimes and ls to see files in the working directory. Type exit to quit the console.

Provision a Database

The sample app requires a database. Provision a Heroku Postgres database, an add-on available through the Elements Marketplace. Add-ons are cloud services that provide out-of-the-box additional services for your application, such as logging, monitoring, databases, and more.

An essential-0 Postgres size costs $5 a month, prorated to the minute. At the end of this tutorial, we prompt you to delete your database to minimize costs. For production apps in Private Spaces, you might want to use a private database plan such as private-0 or higher.

$ heroku addons:create heroku-postgresql:essential-0 --confirm whispering-river-68900 -- --region us
Creating heroku-postgresql:essential-0 on whispering-river-68900...
Creating heroku-postgresql:essential-0 on whispering-river-68900... ~$0.007/hour (max $5/month)
Database should be available soon
postgresql-regular-63143 is being created in the background. The app will restart when complete...
Use heroku addons:info postgresql-regular-63143 to check creation progress
Use heroku addons:docs heroku-postgresql to view documentation

You can wait for the database to provision by running this command:

$ heroku pg:wait

After that command exits, your Heroku app can access the Postgres database. The DATABASE_URL environment variable stores your credentials, which your app is configured to connect to. You can see all the add-ons provisioned with the addons command:

$ heroku addons

 Add-on                                       Plan        Price        Max price State
 ──────────────────────────────────────────── ─────────── ──────────── ───────── ───────
 heroku-postgresql (postgresql-regular-63143) essential-0 ~$0.007/hour $5/month  created
  └─ as DATABASE

The table above shows add-ons and the attachments to the current app (whispering-river-68900) or other apps.

Use a Database

Listing the config vars for your app displays the URL that your app uses to connect to the database, DATABASE_URL:

$ heroku config
DATABASE_URL: postgres://xx:yyy@host:5432/d8slm9t7b5mjnd
...

Heroku also provides a pg command that shows a lot more information:

$ heroku pg
=== DATABASE_URL

Plan:                  essential-0
Status:                Available
Connections:           unknown/20
PG Version:            16.4
Created:               2025-04-04 01:37
Data Size:             unknown usage / 1 GB (In compliance)
Tables:                0/4000 (In compliance)
Fork/Follow:           Unsupported
Rollback:              Unsupported
Continuous Protection: Off
Add-on:                postgresql-regular-63143

The example app you deployed already has database functionality. It has a controller and database model for movies, used by your app’s /movies page. You can visit the page by appending /movies to your app’s URL, or with Heroku’s open command:

$ heroku open movies

If you visit the URL, you see an error page appear. Check out the error message using heroku logs to see something like this:

...
app[web.1]: fail: Microsoft.EntityFrameworkCore.Database.Command[20102]
app[web.1]: Failed executing DbCommand (24ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
app[web.1]: SELECT m."Id", m."Genre", m."Price", m."ReleaseDate", m."Title"
app[web.1]: FROM "Movie" AS m
app[web.1]: fail: Microsoft.EntityFrameworkCore.Query[10100]
app[web.1]: An exception occurred while iterating over the results of a query for context type 'GettingStarted.Data.GettingStartedMovieContext'.
app[web.1]: Npgsql.PostgresException (0x80004005): 42P01: relation "Movie" does not exist
...

This error indicates that while we could connect to the database, the Movie table wasn’t found. You can fix that error by running Frontend/bin/publish/efbundle. The example app already built an efbundle executable when you deployed the application (inspect the Frontend/Frontend.csproj file for more details), which can be used to migrate the database.

To execute this command on Heroku, run it in a one-off dyno like so::

$ heroku run Frontend/bin/publish/efbundle
Running Frontend/bin/publish/efbundle on ⬢ whispering-river-68900... up, run.4436
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (41ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT EXISTS (
          SELECT 1 FROM pg_catalog.pg_class c
          JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace
          WHERE n.nspname='public' AND
                c.relname='__EFMigrationsHistory'
      )
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT EXISTS (
          SELECT 1 FROM pg_catalog.pg_class c
          JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace
          WHERE n.nspname='public' AND
                c.relname='__EFMigrationsHistory'
      )
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (35ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "__EFMigrationsHistory" (
          "MigrationId" character varying(150) NOT NULL,
          "ProductVersion" character varying(32) NOT NULL,
          CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
      );
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT EXISTS (
          SELECT 1 FROM pg_catalog.pg_class c
          JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace
          WHERE n.nspname='public' AND
                c.relname='__EFMigrationsHistory'
      )
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "MigrationId", "ProductVersion"
      FROM "__EFMigrationsHistory"
      ORDER BY "MigrationId";
Applying migration '20240216004219_InitialCreate'.
info: Microsoft.EntityFrameworkCore.Migrations[20402]
      Applying migration '20240216004219_InitialCreate'.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (18ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Movie" (
          "Id" INTEGER GENERATED ALWAYS AS IDENTITY,
          "Title" TEXT,
          "ReleaseDate" DATE NOT NULL,
          "Genre" TEXT,
          "Price" NUMERIC NOT NULL,
          CONSTRAINT "PK_Movie" PRIMARY KEY ("Id")
      );
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
      VALUES ('20240216004219_InitialCreate', '8.0.10');
Done.

Now if you visit the /movies page of your app again, you can list and create movie records.

If you have Postgres installed locally, you can also interact directly with the database. For example, here’s how to connect to the database using psql and execute a query:

$ heroku pg:psql
d8slm9t7b5mjnd=> \x
d8slm9t7b5mjnd=> select * from "Movie";
-[ RECORD 1 ]----------------
Id          | 1
Title       | Blade Runner
ReleaseDate | 1982-06-25
Genre       | Science Fiction
Price       | 19.99
...

Read more about Heroku PostgreSQL.

Define Config Vars

Heroku lets you externalize configuration by storing data such as encryption keys or external resource addresses in config vars.

We expose config vars as environment variables to the application.

To demonstrate this, modify Frontend/Program.cs to add a simple /hello endpoint that returns a greeting using the value of the HELLO_FROM environment variable:

// ...
app.MapGet("/hello", () => $"Hello from {Environment.GetEnvironmentVariable("HELLO_FROM")}!");

app.Run();

Run the app with dotnet run --project Frontend -e "HELLO_FROM=Me" and visit http://localhost:5289/hello to see “Hello from Me!”.

To set the config var on Heroku, execute the following:

$ heroku config:set HELLO_FROM=Heroku
Setting HELLO_FROM and restarting whispering-river-68900...
Setting HELLO_FROM and restarting whispering-river-68900... done, v5
HELLO_FROM: Heroku

View the app’s config vars using heroku config:

$ heroku config
HELLO_FROM: Heroku
...

To see this change in action, deploy your changed application to Heroku.

Delete Your App

Remove the app from your account. We only charge you for the resources you used.

This action permanently deletes your application and any add-ons attached to it.

$ heroku apps:destroy

You can confirm that your app is gone with this command:

$ heroku apps --all

Next Steps

You now know how to configure and deploy a .NET app, view logs, and start a console.

To learn more, see:

  • How Heroku Works
  • Preparing a Codebase for Heroku Deployment
  • Heroku .NET Support Reference
  • .NET Behavior in Heroku

Information & Support

  • Getting Started
  • Documentation
  • Changelog
  • Compliance Center
  • Training & Education
  • Blog
  • Support Channels
  • Status

Language Reference

  • Node.js
  • Ruby
  • Java
  • PHP
  • Python
  • Go
  • Scala
  • Clojure
  • .NET

Other Resources

  • Careers
  • Elements
  • Products
  • Pricing
  • RSS
    • Dev Center Articles
    • Dev Center Changelog
    • Heroku Blog
    • Heroku News Blog
    • Heroku Engineering Blog
  • Twitter
    • Dev Center Articles
    • Dev Center Changelog
    • Heroku
    • Heroku Status
  • Github
  • LinkedIn
  • © 2025 Salesforce, Inc. All rights reserved. Various trademarks held by their respective owners. Salesforce Tower, 415 Mission Street, 3rd Floor, San Francisco, CA 94105, United States
  • heroku.com
  • Legal
  • Terms of Service
  • Privacy Information
  • Responsible Disclosure
  • Trust
  • Contact
  • Cookie Preferences
  • Your Privacy Choices