Getting Started on Heroku with .NET
Introduction
Complete this tutorial to deploy a sample ASP.NET Core and .NET application to Heroku on the Cedar generation.
The tutorial assumes that you have:
- A verified Heroku Account
- An Eco dynos plan subscription (recommended)
- .NET SDK 8.0+ installed locally - you can download the latest “Build apps - SDK” installer for your OS and architecture on the .NET 8.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
The Heroku CLI requires Git, the popular version control system. If you don’t already have Git installed, complete the following before proceeding:
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.
Download and run the installer for your platform:
$ brew tap heroku/brew && brew install heroku
Download the appropriate installer for your Windows installation:
More installation options for the Heroku CLI can be found 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
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, powerful-retreat-78303
https://powerful-retreat-78303-1700992c6e80.herokuapp.com/ | https://git.heroku.com/powerful-retreat-78303.git
Now configure the app to use the .NET buildpack:
$ heroku buildpacks:add heroku/dotnet
Buildpack added. Next release on powerful-retreat-78303 will use heroku/dotnet.
Run git push heroku main to create a new release using this buildpack.
When you create an app, a git remote called heroku
is also created and associated with your local git repository. Git remotes are versions of your repository that live on other servers. You deploy your app by pushing its code to that special Heroku-hosted remote associated with your app.
Heroku generates a random name for your app, in this case, powerful-retreat-78303
. You can specify your own app name.
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.
$ heroku addons:create heroku-postgresql:essential-0
Creating heroku-postgresql:essential-0 on powerful-retreat-78303...
Creating heroku-postgresql:essential-0 on powerful-retreat-78303... ~$0.007/hour (max $5/month)
Database should be available soon
postgresql-perpendicular-82865 is being created in the background. The app will restart when complete...
Use heroku addons:info postgresql-perpendicular-82865 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
Waiting for database postgresql-perpendicular-82865... Provisioning
Waiting for database postgresql-perpendicular-82865... Available
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-perpendicular-82865) essential-0 ~$0.007/hour $5/month created
└─ as DATABASE
The table above shows add-ons and the attachments to the current app (powerful-retreat-78303) or other apps.
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 42 paths from 8c1c0a9
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Building on the Heroku-24 stack
remote: -----> Using buildpack: heroku/dotnet
remote: -----> .NET app detected
remote: -----> SDK version detection
remote: Detected .NET file to publish: `/tmp/build_d0696ca9/GettingStarted.sln`
remote: Inferring version requirement from `/tmp/build_d0696ca9/GettingStarted.sln`
remote: Detected version requirement: `^8.0`
remote: Resolved .NET SDK version `8.0.404` (linux-amd64)
remote: -----> SDK installation
remote: Downloading SDK from https://download.visualstudio.microsoft.com/download/pr/4e3b04aa-c015-4e06-a42e-05f9f3c54ed2/74d1bb68e330eea13ecfc47f7cf9aeb7/dotnet-sdk-8.0.404-linux-x64.tar.gz ... (0.9s)
remote: Verifying SDK checksum
remote: Installing SDK
remote: -----> Publish solution
remote: Using `Release` build configuration
remote: Running `dotnet publish /tmp/build_d0696ca9/GettingStarted.sln --runtime linux-x64 "-p:PublishDir=bin/publish"`
remote:
remote: Determining projects to restore...
remote: Restored /tmp/build_d0696ca9/Frontend/Frontend.csproj (in 11.68 sec).
remote: Frontend -> /tmp/build_d0696ca9/Frontend/bin/Release/net8.0/linux-x64/Frontend.dll
remote: Frontend -> /tmp/build_d0696ca9/Frontend/bin/publish/
remote: Restoring .NET tools
remote: Tool 'dotnet-ef' (version '8.0.10') was restored. Available commands: dotnet-ef
remote:
remote: Restore was successful.
remote: Publishing executable database migration bundle
remote: Build started...
remote: Build succeeded.
remote: Building bundle...
remote: Done. Migrations Bundle: /tmp/build_d0696ca9/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 (29.2s)
remote: -----> Setting launch table
remote: Detecting process types from published artifacts
remote: Added `Frontend`: bash -c cd Frontend/bin/publish; ./Frontend --urls http://*:$PORT
remote: -----> Done (finished in 33.4s)
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote: Default types for buildpack -> Frontend
remote:
remote: -----> Compressing...
remote: Done: 116.2M
remote: -----> Launching...
remote: Released v4
remote: https://powerful-retreat-78303-1700992c6e80.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/powerful-retreat-78303.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
2024-12-01T06:23:05.075482+00:00 app[api]: Release v1 created by user developer@example.com
2024-12-01T06:23:05.075482+00:00 app[api]: Initial release by user developer@example.com
2024-12-01T06:23:05.421897+00:00 app[api]: Release v2 created by user developer@example.com
2024-12-01T06:23:05.421897+00:00 app[api]: Enable Logplex by user developer@example.com
2024-12-01T06:25:09.935885+00:00 app[api]: Release v3 created by user heroku-postgresql@addons.heroku.com
2024-12-01T06:25:09.935885+00:00 app[api]: @ref:postgresql-perpendicular-82865 completed provisioning, setting DATABASE_URL. by user heroku-postgresql@addons.heroku.com
2024-12-01T06:25:15.000000+00:00 app[api]: Build started by user developer@example.com
2024-12-01T06:26:07.733673+00:00 app[api]: Deploy 73292de6 by user developer@example.com
2024-12-01T06:26:07.733673+00:00 app[api]: Release v4 created by user developer@example.com
2024-12-01T06:26:07.747616+00:00 app[api]: Scaled to Frontend@0:Basic web@1:Basic by user developer@example.com
2024-12-01T06:26:08.000000+00:00 app[api]: Build succeeded
2024-12-01T06:26:11.774452+00:00 heroku[web.1]: Starting process with command `cd Frontend/bin/publish/; ./Frontend --urls http://*:50818`
2024-12-01T06:26:12.652983+00:00 app[web.1]: warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
2024-12-01T06:26:12.653000+00:00 app[web.1]: No XML encryptor configured. Key {a779e396-e672-4747-ba9b-5b50d7312c6e} may be persisted to storage in unencrypted form.
2024-12-01T06:26:12.688009+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[14]
2024-12-01T06:26:12.688010+00:00 app[web.1]: Now listening on: http://[::]:50818
2024-12-01T06:26:12.689503+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[0]
2024-12-01T06:26:12.689504+00:00 app[web.1]: Application started. Press Ctrl+C to shut down.
2024-12-01T06:26:12.690088+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[0]
2024-12-01T06:26:12.690088+00:00 app[web.1]: Hosting environment: Production
2024-12-01T06:26:12.690097+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[0]
2024-12-01T06:26:12.690097+00:00 app[web.1]: Content root path: /app/Frontend/bin/publish
2024-12-01T06:26:12.979428+00:00 heroku[web.1]: State changed from starting to up
2024-12-01T06:26:14.068563+00:00 heroku[router]: at=info method=GET path="/" host=powerful-retreat-78303-1700992c6e80.herokuapp.com request_id=25b31161-04da-44cd-b819-66473cc3ef56 fwd="204.14.236.213" dyno=web.1 connect=0ms service=76ms status=200 bytes=8739 protocol=https
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 39 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 .
warning: in the working copy of 'bgworker/Program.cs', CRLF will be replaced by LF the next time Git touches it
warning: in the working copy of 'bgworker/bgworker.csproj', CRLF will be replaced by LF the next time Git touches it
Commit the changes to the repository:
$ git commit -m "Added bgworker"
[main 60d9cfc] Added bgworker
3 files changed, 18 insertions(+)
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 launch a REPL process attached to your local terminal for experimenting in your app’s environment:
$ heroku run bash
Running bash on ⬢ web.1... up, run.1360 (Eco)
~ $ dotnet --list-runtimes
Microsoft.AspNetCore.App 8.0.11 [/app/.heroku/cnb/dotnet/layers/runtime/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 8.0.11 [/app/.heroku/cnb/dotnet/layers/runtime/shared/Microsoft.NETCore.App]
~ $ ls
Frontend Procfile docker-compose.yml
GettingStarted.sln README.md
LICENSE.txt bgworker
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.
~ $ exit
exit
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.3
Created: 2024-12-01 06:23
Data Size: unknown usage / 1 GB (In compliance)
Tables: 0/4000 (In compliance)
Fork/Follow: Unsupported
Rollback: Unsupported
Continuous Protection: Off
Add-on: postgresql-perpendicular-82865
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 ⬢ powerful-retreat-78303... 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.
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: