Getting Started on Heroku Fir with .NET
Introduction
Complete this tutorial to deploy a sample ASP.NET Core and .NET application to Heroku on the Fir generation.
The tutorial assumes that you have:
- A verified Heroku Account
- An existing Fir Private Space
- A team admin or member role that has the
app creation
permission on the space. - An SSH key added to your Heroku account
- .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 1X-Classic dynos and an Essential-0 Postgres database to complete this tutorial. Delete all resources after completing the tutorial.
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 in a Fir Space
Delete your app and database as soon as you’re done to control costs.
You can get a list of all Heroku spaces by running $ heroku spaces
Create an app on Heroku to prepare the platform to receive your source code by replacing <space-name>
with the name of your Fir space in the command below:
$ heroku create --space <space-name>
Creating app in space <space name>...
Creating app in space <space name>... done, secret-plateau-96887
http://secret-plateau-96887-53d8dda404a6.herokuapp.com/ | https://git.heroku.com/secret-plateau-96887.git
When you create an app, a Git remote called heroku
also gets 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, secret-plateau-96887
. 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 secret-plateau-96887...
Creating heroku-postgresql:essential-0 on secret-plateau-96887... ~$0.007/hour (max $5/month)
Database should be available soon
postgresql-octagonal-89270 is being created in the background. The app will restart when complete...
Use heroku addons:info postgresql-octagonal-89270 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-octagonal-89270... Provisioning
Waiting for database postgresql-octagonal-89270... 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-octagonal-89270) essential-0 ~$0.007/hour $5/month created
└─ as DATABASE
The table above shows add-ons and the attachments to the current app (secret-plateau-96887) 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: Extracting source
remote: Image with name "secret-plateau-96887/builds" not found
remote: 2 of 3 buildpacks participating
remote: heroku/dotnet 0.1.8
remote: heroku/procfile 3.1.2
remote:
remote: ## Heroku .NET Buildpack
remote:
remote: - SDK version detection
remote: - Detected .NET file to publish: `/workspace/GettingStarted.sln`
remote: - Inferring version requirement from `/workspace/GettingStarted.sln`
remote: - Detected version requirement: `^8.0`
remote: - Resolved .NET SDK version `8.0.404` (linux-arm64)
remote: - SDK installation
remote: - Downloading SDK from https://download.visualstudio.microsoft.com/download/pr/5ac82fcb-c260-4c46-b62f-8cde2ddfc625/feb12fc704a476ea2227c57c81d18cdf/dotnet-sdk-8.0.404-linux-arm64.tar.gz .... (1.0s)
remote: - Verifying SDK checksum
remote: - Installing SDK
remote: - Publish solution
remote: - Using `Release` build configuration
remote: - Running `dotnet publish /workspace/GettingStarted.sln --runtime linux-arm64 "-p:PublishDir=bin/publish"`
remote:
remote: Determining projects to restore...
remote: Restored /workspace/Frontend/Frontend.csproj (in 11.57 sec).
remote: Frontend -> /workspace/Frontend/bin/Release/net8.0/linux-arm64/Frontend.dll
remote: Frontend -> /workspace/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: /workspace/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 (36.1s)
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 41.1s)
remote:
remote: [Discovering process types]
remote: Procfile declares types -> web
remote: Adding layer 'heroku/dotnet:runtime'
remote: Adding layer 'buildpacksio/lifecycle:launch.sbom'
remote: Added 1/1 app layer(s)
remote: Adding layer 'buildpacksio/lifecycle:launcher'
remote: Adding layer 'buildpacksio/lifecycle:config'
remote: Adding layer 'buildpacksio/lifecycle:process-types'
remote: Adding label 'io.buildpacks.lifecycle.metadata'
remote: Adding label 'io.buildpacks.build.metadata'
remote: Adding label 'io.buildpacks.project.metadata'
remote: Setting default process type 'web'
remote: Saving secret-plateau-96887/builds...
remote: *** Images (sha256:83c9477b2597537a144095ee36a4e8389229cafe031deed0aed7b03b20662684):
remote: secret-plateau-96887/builds:26b7e376-c1e0-4b57-b673-c139182fd71b
remote: Adding cache layer 'heroku/dotnet:nuget-cache'
remote: Adding cache layer 'heroku/dotnet:sdk'
remote: Uploading cache
remote: Launching...
remote: https://secret-plateau-96887-53d8dda404a6.herokuapp.com/ deployed to Heroku
remote: Verifying deploy... done.
To https://git.heroku.com/secret-plateau-96887.git
* [new branch] main -> main
The app is now deployed. The default dyno size for Fir Private Spaces is 1X-Classic.
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
Fetching logs...
2024-12-01T06:46:24.363349+00:00 app[web-55c7f77bc6-64jkt]: warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
2024-12-01T06:46:24.363395+00:00 app[web-55c7f77bc6-64jkt]: Storing keys in a directory '/home/heroku/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed. For more information go to https://aka.ms/aspnet/dataprotectionwarning
2024-12-01T06:46:24.475112+00:00 app[web-55c7f77bc6-64jkt]: warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
2024-12-01T06:46:24.475152+00:00 app[web-55c7f77bc6-64jkt]: No XML encryptor configured. Key {422ebd4a-4f78-4158-a718-c04a555db9c7} may be persisted to storage in unencrypted form.
2024-12-01T06:46:24.560340+00:00 app[web-55c7f77bc6-64jkt]: info: Microsoft.Hosting.Lifetime[14]
2024-12-01T06:46:24.560385+00:00 app[web-55c7f77bc6-64jkt]: Now listening on: http://[::]:8000
2024-12-01T06:46:24.562025+00:00 app[web-55c7f77bc6-64jkt]: info: Microsoft.Hosting.Lifetime[0]
2024-12-01T06:46:24.562041+00:00 app[web-55c7f77bc6-64jkt]: Application started. Press Ctrl+C to shut down.
2024-12-01T06:46:24.563723+00:00 app[web-55c7f77bc6-64jkt]: info: Microsoft.Hosting.Lifetime[0]
2024-12-01T06:46:24.563756+00:00 app[web-55c7f77bc6-64jkt]: Hosting environment: Production
2024-12-01T06:46:24.564278+00:00 app[web-55c7f77bc6-64jkt]: info: Microsoft.Hosting.Lifetime[0]
2024-12-01T06:46:24.564289+00:00 app[web-55c7f77bc6-64jkt]: Content root path: /workspace/Frontend/bin/publish
2024-12-01T06:46:36.766779+00:00 heroku-router[web]: at=info method=GET path="/" host=secret-plateau-96887-53d8dda404a6.herokuapp.com request_id=164ab171-a3b7-c2dd-e8bd-2353529f17d3 fwd="204.14.236.213" dyno=web-55c7f77bc6-64jkt connect=1ms service=106ms status=200 bytes=8522 protocol=http tls_version=tls1.3
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 38 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 e6826c5] 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
The heroku run
command to launch an interactive one-off dyno is unavailable for Fir. As an alternative, use heroku run:inside
to access a running dyno until we add heroku run
for Fir.
You must add an SSH key to your Heroku account before running this command.
To execute the command you need the name of a currently running process. You can view a list of current processes by running:
$ heroku ps
=== web (1X-Classic): cd Frontend/bin/publish/; ./Frontend --urls http://*:$PORT (1)
web-55c7f77bc6-64jkt: up 2024/12/01 01:46:30 -0500 (~ 2m ago)
You can use that dyno name to run a command such as the interactive terminal bash
by prepending “launcher” to it and executing it like this:
$ heroku run:inside web-55c7f77bc6-64jkt "launcher bash"
Running launcher bash on ⬢ secret-plateau-96887... up, web-55c7f77bc6-64jkt
heroku@web-55c7f77bc6-64jkt:/workspace$ dotnet --list-runtimes
Microsoft.AspNetCore.App 8.0.11 [/layers/heroku_dotnet/runtime/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 8.0.11 [/layers/heroku_dotnet/runtime/shared/Microsoft.NETCore.App]
heroku@web-55c7f77bc6-64jkt:/workspace$ 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:41
Data Size: unknown usage / 1 GB (In compliance)
Tables: 0/4000 (In compliance)
Fork/Follow: Unsupported
Rollback: Unsupported
Continuous Protection: Off
Add-on: postgresql-octagonal-89270
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:
fail: Microsoft.EntityFrameworkCore.Database.Command[20102]
Failed executing DbCommand (24ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT m."Id", m."Genre", m."Price", m."ReleaseDate", m."Title"
FROM "Movie" AS m
fail: Microsoft.EntityFrameworkCore.Query[10100]
An exception occurred while iterating over the results of a query for context type 'GettingStarted.Data.GettingStartedMovieContext'.
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
via run:inside
. 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 inside an existing dyno like so:
$ heroku run:inside web-55c7f77bc6-64jkt "launcher Frontend/bin/publish/efbundle"
Running launcher Frontend/bin/publish/efbundle on ⬢ secret-plateau-96887... up, web-55c7f77bc6-64jkt
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: