Setting Up a Development Environment With Docker For .NET With Postgres SQL

Setting Up a Development Environment With Docker For .NET With Postgres SQL

Setting up the containerized environment for .Net and Postgres SQL in VS Code

In this article, we are going to set up a developer-friendly environment for the asp.net MVC application in visual studio code. In addition, we are going to containerize the MVC application with postgres SQL database along with Pgadmin4. When it comes to development debugging is a crucial task. we also set up debugging functionality to code in the docker container. So, without wasting time let's get started!

What will we learn from this article?

This article guide teaches you how to set up containerized .NET application using docker and how to use it in a developer environment with the debugger.

  • Create a simple dot net MVC application

  • Create a new Dockerfile which contains instructions required to build a. Net image.

  • Set up docker-compose which set up postgres sql database, pgadmin to monitor postgres database.

  • Set up vs code debugger in the visual studio code editor.

Docker

Docker is a software platform that enables rapid development, testing, and deployment of applications. Docker encapsulates software into standardized units called containers that have everything (i.e all dependencies) the software needs to run including libraries, system tools, code, runtime, and settings.

Container

A container is a standard unit of software that packages up code and all its dependencies to that application and runs quickly and reliably from one computing environment to another. We can think container as an isolated process in the operating system which has its own networking and file system. A Docker Container image is a lightweight, standalone, executable package of software that includes everything needed to run an application: code, runtime, system tools, system libraries, and settings.

DotNet

.NET is an open-source developer platform, created by Microsoft, for building many different types of applications. .Net is a free, cross-platform, open-source developer platform for building different types of applications.

With .NET, you can use multiple languages, editors, and libraries to build for web, mobile, desktop, games, IoT, and more. You can write .NET apps in C#, F#, and many other dot net languages.

Software Prerequistes

Before diving into the coding section, we have to make sure the following software is installed. I am using windows 11 for this guide. You can use other operating systems as well.

  1. Docker Desktop with docker-compose

  2. Visual Studio Code

  3. Dotnet Sdk 7

  4. Any Web Browser

Creating New .NET MVC Application

For our sample application, let's create a simple application from a template using .NET CLI. Create a directory in our local machine named sampleapp. Open a cmd and change to that directory. Run the following dotnet new command to create a C# app using the ASP.NET MVC template.

mkdir sampleapp
cd sampleapp
dotnet new mvc -n sampleapp -o src --no-https

Above command will create an src file inside sampleapp folder. Furthermore, inside src, we will get asp.net MVC project folders and files. The further structure will look like following

├── src
├── Controllers
│ └── HomeController.cs
├── Models
│ └── ErrorViewModel.cs
├── Views
│ └── Home
│     └── Index.cshtml
│     └── Privacy.cshtml
├── Program.cs
├── Properties
│ └── launchSettings.json
├── appsettings.Development.json
├── appsettings.json
├── sampleapp.csproj
├── obj
│ ├── sampleapp.csproj.nuget.dgspec.json
│ ├── sampleapp.csproj.nuget.g.props
│ ├── sampleapp.csproj.nuget.g.targets
│ ├── project.assets.json
│ └── project.nuget.cache
└── wwwroot
  ├── css
  ├── favicon.ico
  ├── js
  └── lib

Now, Let's start our application and make sure it's running properly. In the command prompt navigate to the src folder and use the dotnet run command.

cd /path/to/sampleapp/src
dotnet run --urls http://localhost:5000

Ouput similar to the following appears.

Building...
info: Microsoft.Hosting.Lifetime[0]
    Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
    Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
    Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
    Content root path: C:\Users\username\sampleapp\src

Now you will see the following page in the browser:

If you get the result as the above image then you will install mvc project correctly and you

are ready to go further.

Create a Dockerfile

In sampleapp directory, create a file named Dockerfile And paste the following code.

FROM mcr.microsoft.com/dotnet/sdk:7.0 as debug

#install debugger for NET Core
RUN apt-get update
RUN apt-get install -y unzip
RUN curl -sSL https://aka.ms/getvsdbgsh | /bin/sh /dev/stdin -v latest -l ~/vsdbg

RUN mkdir /src/
WORKDIR /src/

COPY ./src/testapp.csproj /src/sampleapp.csproj
RUN dotnet restore

COPY ./src/ /src/
RUN mkdir /out/
RUN dotnet publish --no-restore --output /out/ --configuration Release
EXPOSE 80
CMD dotnet run --urls "http://0.0.0.0:80"

The above Dockerfiel is used for building and running a .NET Core application in a Docker Container.

It starts with the base image mcr.microsoft.com/dotnet/sdk:7.0 and tags it as debug.

In summary above docker file will do the following tasks step by step one at a time.

  1. Installs the debugger for .NET Core by updating the package list and installing unzip, then downloading and installing the latest version of vsdbg.

  2. Creates a directory '/src/' as the working directory.

  3. Copies the project file 'sampleapp.csproj' to '/src/' and restores the dependencies.

  4. Copies the source code to '/src/' and runs the publish command to build and output the application to '/out/'.

  5. Exposes port 80 for external access.

  6. Runs the application with the command dotnet run --urls "http://0.0.0.0:80".

Create docker-compose.yml File

Docker Compose is a tool for defining and running multi-container Docker applications. It allows you to define an application’s services, networks, and volumes in a single file, then start and stop the services using a single command.

Open the sampeapp directory in vs code and create a file named docker-compose.yml . Copy and paste the following contents into the file.

version: "3.0"
services:
  db:
    image: postgres
    restart: always
    ports:
      - "5432"
    volumes:
      - postgres:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_USER: sachin

  pg_admin:
    image: dpage/pgadmin4
    restart: always
    ports:
      - "5555:80"
    volumes:
      - pg_admin:/var/lib/pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: sachin.maharjan@dishhome.com.np
      PGADMIN_DEFAULT_PASSWORD: password

  web:
    container_name: csharp
    build:
      context: .
      target: debug
    ports:
     - "5000:80"
    volumes:
      - ./src:/src/
    depends_on:
     - db

volumes:
  postgres:
  pg_admin:

Save and close the docker-compose.yml file. Add the .dockerignore file and paste the following code.

**/bin/
**/obj/

The sampleapp directory structure should now look like

├── samplefile
│ ├── src/
│ ├── Dockerfile
│ ├── .dockerignore
│ ├── docker-compose.yml

The .dockerignore the file is used in a Docker build context to specify files or directories that should be excluded from the build context. This helps to reduce the size of the build context and improve the build time by excluding files and directories that are not necessary for the build. Any files or directories listed in the .dockerignore file will not be included in the build context that is sent to the Docker daemon for building the image.

The docker-compose.yml the file describes a multi-container application. In our docker-compose.yml file, we have three services db, pgadmin and web.

  • 'db' is a PostgreSQL database service, which uses the official PostgreSQL image. It will be exposed on port 5432 and the data will be persisted in a Docker volume named 'postgres'. The environment variables 'POSTGRES_PASSWORD' and 'POSTGRES_USER' are set to 'password' and 'sachin' respectively.

  • 'pg_admin' is a web-based database administration tool, using the image 'dpage/pgadmin4'. It will be exposed on port 5555 and the data will be persisted in a Docker volume named 'pg_admin'. The environment variables 'PGADMIN_DEFAULT_EMAIL' and 'PGADMIN_DEFAULT_PASSWORD' are set to '' and 'password' respectively.

  • 'web' is a .NET Core web application service. The service is built from the current directory and the build context is set to the 'debug' target. The service will be exposed on port 5000 and the source code will be mounted as a volume in the container at the '/src/' directory. The 'depends_on' key ensures that the 'db' service is started before the 'web' service.

  • The 'volumes' section at the bottom defines two named volumes: 'postgres' and 'pg_admin'.

Database Connection

Let's add the package to allow the app to talk to a database and update the source files. On your local machine, open a terminal, change the directory to the src directory and run the following command:

cd /path/to/dotnet-docker/src
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

In the src directory, create a Models folder. Inside the Models folder create a file named Student.cs and add the following code to Student.cs:

using System;
using System.Collections.Generic;
namespace sampleapp.Models
{
  public class Student
  {
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
  }
}

Save and close the Student.cs file.

In the src directory, create a Data folder. Inside the Data folder create a file named SchoolContext.cs and add the following code to SchoolContext.cs:

using Microsoft.EntityFrameworkCore;
namespace sampleapp.Data
{
   public class SchoolContext : DbContext
   {
      public SchoolContext(DbContextOptions<SchoolContext> options) : base(options) { }
      public DbSet<Models.Student>? Students { get; set; }
   }
}

Save and close the SchoolContext.cs file.

In the Program.cs file located in the src directory, replace the contents with the following code:

using Microsoft.EntityFrameworkCore;
using sampleapp.Data;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
   options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    var context = services.GetRequiredService<ApplicationDbContext>();
    if (context.Database.GetPendingMigrations().Any())
    {
        context.Database.Migrate();
    }
}
app.Run();

Save and close the Program.cs file.

In the appsettings.json file located in the src directory, replace the contents with the following code:

{
   "Logging": {
       "LogLevel": {
           "Default": "Information",
           "Microsoft": "Warning",
           "Microsoft.Hosting.Lifetime": "Information"
       }
   },
   "AllowedHosts": "*",
   "ConnectionStrings": {
       "SchoolContext": "Host=db;Database=my_db;Username=sachin;Password=password"
   }
}

Save and close the appsettings.json file.

In the Index.cshtml file located in the src\Pages directory, replace the contents with the following code:


@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<div class="row mb-auto">
    <p>Student Name is @ViewBag.Student</p>
</div>

Save and close the Index.cshtml file.

In the Index.cshtml.cs file located in the src\Pages directory, replace the contents with the following code:

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using sampleapp.Models;

namespace testapp.Controllers;

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly SchoolDbContext _db;
    public HomeController(ILogger<HomeController> logger, SchoolDbContext db)
    {
        _logger = logger;
        _db = db;
    }

    public IActionResult Index()
    {
        var student = _context.Students?.Where(d=>d.ID==1).FirstOrDefault();
            this.StudentName = $"{s?.FirstMidName} {s?.LastName}";
       ViewBag.Student = student;
        return View();
    }

    public IActionResult Privacy()
    {
        return View();
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}

Save and close the Index.cshtml.cs file.

Now, add migration command to create database and students table in a database named my_db.

dotnet-ef migrations add "InitalMigration"

Note: you need to install dotnet-ef utility tool, for this you can install this using

donet tools install --global dotnet-ef

Now, up the all container using docker-compose up command. cd to sampleapp directory and run the following command.

cd path to sampleapp
docker-compose up --build

If every thing is ok then all three containers web, database and pgadmin will start. Now, go to browser and enter url localhost:5555. you will see the login page of pg admin.

Now, Login into pgadmin using email and password setup in docker-compose.yml file. After that, you will redirect to the home page.

Now right-click on the server and click on to register server option. Then a dialog box will open. In that section, you have to provide the hostname as db port name 5432 and username as sachin and password as password. All credentials are already defined in docker-compose.yml the file.

Now, Open the server menu you will see the database name my_db. Now open that my_db menu you will see the schema section. Under the schema section, you will see tables. Now add a row in the table named students.

so, If you open a browser and enter localhost:5000 you will see the home page with the first name and last name that you have inserted in the database.

Enabling Debugging In VS Code

For enabling debugging in vs code, first, we need the following extensions in vs code

  1. VS Code Extension for Docker

  2. VS Code Launch configuration

  3. C# extension for VS Code

Click on to debug button on the right of the visual studio code. Then You will have the option to create a launch.json file. Click on it to create a launch.json.

Then it will create .vscode under sampleapp directory. when we open .vscode directory we will see the launch.json file. Open the file and paste the code below:

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": ".NET Core Docker Attach",
            "type": "coreclr",
            "request": "attach",
            "processId": "${command:pickRemoteProcess}",
            "pipeTransport": {
                "pipeProgram": "docker",
                "pipeArgs": [ "exec", "-i", "csharp" ],
                "debuggerPath": "/root/vsdbg/vsdbg",
                "pipeCwd": "${workspaceRoot}",
                "quoteArgs": false
            },
            "sourceFileMap": {
                "/src": "${workspaceRoot}/src/"
            }
        },

    ]
}

Now, if you put breakpoints in any c# code and hit the green run button on debug section. Then execution stops at that breakpoint. But you have to choose the correct process during the prompt which pops out while you click the green run button.

The above animation is for node.js, I have kept it just to simulate how it is done.

That is, it! I hope you found this helpful. If you have any questions or are confused about anything, you can reach out to me or you can check out the sample project on GitHub.

Thank you!

Happy Coding :)