BiblioTech already has a mature domain, a decoupled architecture, a complete API, and a test suite — unit and integration — confirming that everything works as expected. One last step remains, the one that turns all that work into something a real user can use: deploying it. This lesson publishes the API with dotnet publish, packages it into a simple Docker container, configures its behavior differently depending on the environment (development versus production), and touches, without going into detail on any specific platform, on how all of this fits into a continuous integration and deployment pipeline. It's the course's final lesson: it closes not just Module 9, but the entire journey that began with a simple "Hello World" in Module 1's first lesson.

Contents

  1. dotnet publish: from source code to a deployable artifact
  2. Basic containerization with Docker
  3. Per-environment configuration: appsettings.json and environment variables
  4. The production connection string
  5. Basic CI/CD considerations
  6. BiblioTech, deployed

  1. dotnet publish: from source code to a deployable artifact

Until now, every lesson in the course ran with dotnet run, which compiles and starts the application in a single step meant for development. dotnet publish, by contrast, generates the application's deployment-ready version: the exact files needed to run it on another machine, with no need for the source code or the full .NET SDK, only the runtime:

dotnet publish BiblioTech.Api -c Release -o ./published
  • -c Release: compiles in Release mode instead of Debug (the default used in development), with optimizations enabled and without the extra information that makes step-by-step debugging easier (Lesson 4) but isn't needed in production.
  • -o ./published: the output folder with the result: the assemblies (.dll) for BiblioTech.Api, BiblioTech.Domain, and BiblioTech.Persistence, their dependencies, and an entry-point executable.

The contents of ./published are exactly what gets copied to a server, or packaged inside a Docker container in the next section: the source code, BiblioTech.Tests, and any development project are no longer needed to run the application from there.

  1. Basic containerization with Docker

Docker packages an application together with everything it needs to run (the .NET runtime, in this case) into a self-contained unit called an image, which runs identically on any machine with Docker installed, regardless of anything else that machine has installed. A simple Dockerfile for BiblioTech.Api:

# Stage 1: build and publish the application with the full .NET SDK
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

COPY . .
RUN dotnet restore BiblioTech.Api
RUN dotnet publish BiblioTech.Api -c Release -o /published

# Stage 2: the final image only needs the runtime, not the full SDK
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app

COPY --from=build /published .

EXPOSE 8080
ENTRYPOINT ["dotnet", "BiblioTech.Api.dll"]
docker build -t bibliotech-api .
docker run -p 8080:8080 bibliotech-api

This Dockerfile uses a technique called a multi-stage build: the first stage (build) uses the full SDK image (heavier) only to compile and publish; the second stage (final), the one actually distributed and run, starts from a much lighter image that only brings the ASP.NET Core runtime, without the build tools no longer needed once /published has been generated. docker run -p 8080:8080 exposes the container's port 8080 on the host machine's port 8080, letting you access the API exactly as if it were running locally with dotnet run.

  1. Per-environment configuration: appsettings.json and environment variables

ASP.NET Core automatically distinguishes between environments (development, production...) and loads the corresponding configuration by combining several files and sources, in a specific priority order:

BiblioTech.Api/
├── appsettings.json               // base configuration, common to all environments
├── appsettings.Development.json   // only applies in the "Development" environment (local dotnet run)
└── appsettings.Production.json    // only applies in the "Production" environment (deployed)
// appsettings.json (base)
{
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}
// appsettings.Development.json: more detailed logging locally
{
  "ConnectionStrings": {
    "BiblioTech": "Data Source=bibliotech-dev.db"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Debug"
    }
  }
}
// appsettings.Production.json: no connection string here (see section 4)
{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  }
}
Configuration source When it applies Priority
appsettings.json Always, as the base Lowest
appsettings.{Environment}.json Only if ASPNETCORE_ENVIRONMENT matches {Environment} Medium
Environment variables Whenever they're defined High
Command-line arguments Only if passed at runtime Highest

This hierarchy lets you, for example, have convenient development defaults in appsettings.Development.json (a local SQLite database), while in production an environment variable overrides that same value without touching any deployed file — exactly what solves Lesson 2's security non-functional requirement (not leaving sensitive data in the source code or in version-controlled configuration files).

  1. The production connection string

The production database's connection string (Module 5, Entity Framework Core over SQLite, Lesson 2's decision) is exactly the kind of data that must never be written directly into appsettings.Production.json or into any file uploaded to version control. Instead, it's defined as an environment variable on the server or inside the Docker container itself:

export ConnectionStrings__BiblioTech="Data Source=/data/bibliotech-production.db"

docker run -p 8080:8080 \
  -e ConnectionStrings__BiblioTech="Data Source=/data/bibliotech-production.db" \
  -e ASPNETCORE_ENVIRONMENT=Production \
  bibliotech-api

The double underscore (__) in ConnectionStrings__BiblioTech is how ASP.NET Core translates an environment variable's hierarchy into the same nested structure it would have in JSON ("ConnectionStrings": { "BiblioTech": "..." }). The Program.cs code already written in 09-03 (builder.Configuration.GetConnectionString("BiblioTech")) needs no change at all: it always reads the same way, regardless of whether the final value came from a development JSON file or a production environment variable. This is the same underlying idea that motivated ILibraryRepository in Module 8: the code that consumes a value shouldn't need to know exactly where that value comes from.

  1. Basic CI/CD considerations

CI/CD (Continuous Integration/Continuous Deployment) is the practice of automating, through a pipeline, the steps from a code change to its deployment, instead of running them by hand every time. A typical pipeline for BiblioTech, without tying itself to any specific platform (GitHub Actions, GitLab CI, Azure DevOps, and others all offer variants of the same thing), would chain these steps:

flowchart LR
    A["Code change<br/>(git push)"] --> B["Build<br/>dotnet build"]
    B --> C["Test<br/>dotnet test<br/>(unit + integration, Lesson 4)"]
    C --> D{"All passing?"}
    D -->|"Yes"| E["Publish<br/>dotnet publish + docker build"]
    D -->|"No"| F["Pipeline stops<br/>nothing broken gets deployed"]
    E --> G["Deployment<br/>(specific platform, beyond this lesson's scope)"]

The core value of this pipeline isn't the automation itself, but the guarantee it provides: no change reaches production without having gone, automatically and with no manual step, through exactly the same tests run in Lesson 4. That lesson's quality checklist stops depending on someone remembering to run it by hand before every deployment.

  1. BiblioTech, deployed

With this, BiblioTech completes its journey: a publishable Docker image built with docker build, configured differently in development and production with no code changes, with its production connection string outside the source code, and with a clear path (though not implemented in detail in this course) toward a CI/CD pipeline that automatically verifies every change before deploying it. The system that started as a console exercise in Module 1 ends up being a real application, with an HTTP API, database persistence, automated tests, and a reproducible deployment process.

Common Mistakes and Tips

  • Writing the production connection string directly in appsettings.Production.json: that file is usually version-controlled along with the rest of the source code; any sensitive production data must arrive through an environment variable or a secrets manager, never written there.
  • Publishing in Debug mode instead of Release: this loses the compiler's optimizations and publishes extra information meant only for debugging in development (Lesson 4), not for running in production.
  • Using a single Docker stage with the full SDK for the final image: it works, but produces an image much heavier than necessary, with build tools no longer used once the application is published; the multi-stage build from section 2 solves this with no extra effort.
  • Tip: before considering the deployment complete, explicitly confirm this lesson's three points separately: that dotnet publish generates the correct artifact, that the Docker image starts up and responds locally with docker run, and that the production configuration contains no sensitive data written directly into a version-controlled file.

Exercises

  1. Add an ASPNETCORE_ENVIRONMENT=Production environment variable to section 2's Dockerfile using the ENV instruction, so that any container built from this image starts in production mode by default, unless told otherwise when running docker run.

  2. Describe, in your own words and with no need to configure any real platform, the four steps of section 5's CI/CD pipeline applied to a concrete change: adding the GET /members/{id} endpoint you wrote in the previous lesson's exercise 1.

Solutions

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app

ENV ASPNETCORE_ENVIRONMENT=Production

COPY --from=build /published .

EXPOSE 8080
ENTRYPOINT ["dotnet", "BiblioTech.Api.dll"]

(1) Build: when git push runs with the new endpoint, the pipeline runs dotnet build over the whole solution, confirming it compiles with no errors. (2) Test: it runs dotnet test, including both Module 8's unit tests and Lesson 4's integration tests — if the new endpoint had no integration test of its own, it would be good practice to add one before this step. (3) All passing: if any test fails, the pipeline stops here and the change doesn't move forward. (4) Publish and deploy: only if the previous step succeeded, dotnet publish and docker build run to generate the new image, which is then deployed according to whichever platform was chosen.

Conclusion

This is the end of the C# Programming course. The journey started in Module 1 with setting up the development environment and a first program that printed "Hello World" to the console; it continued through Module 2's control structures and exception handling; it built, in Module 3, BiblioTech's first real object model — LibraryItem, Book, Magazine, Member, Loan — with classes, inheritance, polymorphism, and encapsulation; it expanded that in Module 4 with interfaces, generics, collections, LINQ, and asynchronous programming; it gave it persistent memory in Module 5, with four different persistence mechanisms and a first connection to external services; it explored, in Module 6, .NET's more internal capabilities — reflection, attributes, memory management, and concurrency; it gave it a face, in Module 7, with five different interface technologies, desktop, web, and mobile; and it matured its design in Module 8, with coding standards, design patterns, dependency injection, unit tests, and disciplined refactoring.

This last module has done nothing more than bring that whole path together into a single, complete system: clear requirements, an architecture chosen with judgment, an API exposing the domain built over nine modules, a test suite backing every change, and a reproducible deployment process that reliably carries that code all the way to where someone can actually use it.

From "Hello World" to an application deployed in production: that's the complete journey of this course, and it's also, in essence, the journey of any real software project. What sustains that journey isn't having memorized C# syntax, but having practiced, over and over on the same domain, making design decisions, decoupling responsibilities, trusting tests over luck, and thinking about whoever will use the code as much as whoever writes it. Those are the tools that will keep being useful long after any specific syntax detail is obsolete. Congratulations on making it this far: BiblioTech is now finished, but what you've learned building it doesn't have to stop here. The next C# project is now entirely up to you.

© Copyright 2026. All rights reserved