Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/aws-samples/legacy-cycle-store-mvc-app/llms.txt

Use this file to discover all available pages before exploring further.

Once the Cycle Store is running on ASP.NET Core 3.1, it is no longer tied to Windows or IIS. ASP.NET Core applications run on the cross-platform Kestrel web server, which means they can run inside a Linux Docker container — a significant operational improvement over the original .NET Framework version. Containerization brings reproducible builds, environment parity between development and production, and straightforward deployment to AWS managed container services.

Why Containerize?

Running the application in a Docker container provides several advantages over the legacy IIS-hosted deployment model:
  • Portable deployment — the image encapsulates the application, its runtime, and all dependencies. It runs identically on a developer laptop, a CI/CD pipeline, and an AWS production environment.
  • Consistent environment — eliminates “works on my machine” issues caused by differences in installed .NET Framework versions, IIS configuration, or Windows patches.
  • AWS-native scaling — container images deploy directly to Amazon Elastic Container Service (ECS) on Fargate, Amazon Elastic Kubernetes Service (EKS), or AWS App Runner, all of which provide automatic scaling without managing EC2 instances.
  • No IIS dependency — the legacy app required Windows Server with IIS installed and configured. The containerized ASP.NET Core app uses Kestrel and runs on any Linux host.
  • Immutable releases — each build produces a versioned, immutable image tag pushed to Amazon ECR. Rolling back means redeploying a previous tag — no manual patching of server configuration.

Dockerfile

The Cycle Store uses a multi-stage Docker build. The first stage uses the full .NET SDK image to compile and publish the application. The second stage uses the smaller ASP.NET Core runtime image to create the final deployable image, keeping the image size minimal by excluding the SDK and build tooling.
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:3.1 AS build
WORKDIR /app

# Restore dependencies first to leverage Docker layer caching
COPY *.csproj ./
RUN dotnet restore

# Copy remaining source and publish a Release build
COPY . .
RUN dotnet publish -c Release -o /app/publish

# Stage 2: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:3.1 AS runtime
WORKDIR /app
COPY --from=build /app/publish .

# Expose the default Kestrel port
EXPOSE 80

ENTRYPOINT ["dotnet", "AdventureWorksMVC.dll"]
Place this Dockerfile in the root of the ASP.NET Core project directory (alongside the .csproj file). The dotnet publish step compiles all referenced projects and produces a self-contained output folder — the second stage copies only that output, so no source code is present in the final image.
This page covers the containerization approach planned for Part 3 of the modernization series. The complete container deployment guide — including ECS task definitions, ALB configuration, and auto-scaling — will be published in a separate repository that builds on the ASP.NET Core migration completed in Part 2.

Environment Variables

The containerized application must not have the database connection string baked into its image. appsettings.json is committed to source control and embedded in the image, so it should contain only non-sensitive defaults or placeholder values. At runtime, pass the real connection string as an environment variable. ASP.NET Core’s configuration system automatically maps environment variables to IConfiguration keys. Double underscores (__) act as hierarchy separators, so ConnectionStrings__CycleStore maps to ConnectionStrings:CycleStore in appsettings.json:
docker run \
  -p 8080:80 \
  -e "ConnectionStrings__CycleStore=Server=<rds-endpoint>;Database=CYCLE_STORE;User Id=DBUser;Password=<password>;MultipleActiveResultSets=True;" \
  cyclestore:latest
The application reads the connection string through Configuration.GetConnectionString("CycleStore") in Startup.cs — no code changes are required to support environment variable overrides. The configuration system resolves values in priority order: environment variables override appsettings.json, so the runtime variable always wins.
Never embed database credentials in the Docker image itself — not in appsettings.json, not in the Dockerfile, and not as ENV instructions in the Dockerfile. Images pushed to ECR may be accessible to multiple team members. Use environment variables injected at runtime, AWS Secrets Manager, or ECS task-level secrets to supply credentials securely.

Build and Run Locally

Before pushing to AWS, verify the image builds correctly and can reach the RDS instance from your local machine.
1

Build the Docker image

Run the following command from the directory containing the Dockerfile. The -t flag tags the image with a name and version:
docker build -t cyclestore:latest .
Docker executes both stages. The first run takes longer because it restores NuGet packages; subsequent builds use the cached restore layer unless the .csproj changes.
2

Run the container with the connection string

Start a container from the image, injecting the RDS connection string as an environment variable. Map container port 80 to local port 8080:
docker run -d \
  --name cyclestore-local \
  -p 8080:80 \
  -e "ASPNETCORE_ENVIRONMENT=Development" \
  -e "ConnectionStrings__CycleStore=Server=<rds-endpoint>;Database=CYCLE_STORE;User Id=DBUser;Password=<password>;MultipleActiveResultSets=True;" \
  cyclestore:latest
Setting ASPNETCORE_ENVIRONMENT=Development enables the developer exception page if the app fails to start, making connection errors easier to diagnose.
3

Access the application

Open a browser and navigate to http://localhost:8080. The Cycle Store home page should load, pulling product category data from the RDS instance. To view container logs if something goes wrong:
docker logs cyclestore-local

Push to Amazon ECR

Amazon Elastic Container Registry (ECR) is a fully managed Docker registry integrated with AWS IAM for authentication. Once the image is in ECR, it can be referenced by ECS, EKS, or App Runner deployment configurations.
1

Create an ECR repository

Create a private repository for the Cycle Store image. Replace <region> and <account-id> with your values:
aws ecr create-repository \
  --repository-name cyclestore \
  --region <region>
The command returns the repository URI in the format <account-id>.dkr.ecr.<region>.amazonaws.com/cyclestore. Note this URI — you will use it in the next steps.
2

Authenticate Docker to ECR

Retrieve a temporary authentication token and configure the Docker daemon to use it. The token expires after 12 hours:
aws ecr get-login-password --region <region> | \
  docker login --username AWS --password-stdin \
  <account-id>.dkr.ecr.<region>.amazonaws.com
3

Tag the local image with the ECR repository URI

Tag the image you built locally with the full ECR repository URI:
docker tag cyclestore:latest \
  <account-id>.dkr.ecr.<region>.amazonaws.com/cyclestore:latest
4

Push the image to ECR

Push the tagged image to the ECR repository:
docker push \
  <account-id>.dkr.ecr.<region>.amazonaws.com/cyclestore:latest
After the push completes, the image is available in ECR and can be referenced in ECS task definitions or App Runner service configurations using the full URI.

RDS Connectivity from Container

The CYCLE_STORE RDS instance is deployed by the CloudFormation template provided in this repository. For ease of initial setup, the template configures the RDS Security Group to allow inbound traffic on port 1433 (SQL Server) from 0.0.0.0/0 — any IP address. When running the containerized application in a local Docker environment, this permissive rule is what allows your container to reach the RDS instance over the public internet.
The 0.0.0.0/0 inbound rule on the RDS Security Group is intentionally permissive for this demo. It must be restricted before deploying to any non-demo environment. For production deployments:
  • Lock the Security Group inbound rule to the private IP CIDR of the VPC subnet where your containers run.
  • For ECS on Fargate, attach a Security Group to the Fargate tasks and allow inbound 1433 only from that task Security Group.
  • Consider placing RDS in a private subnet with no public accessibility, reachable only from within the VPC.
Leaving port 1433 open to the internet exposes the database to brute-force login attempts and reconnaissance scans.
For ECS deployments in the same VPC as the RDS instance, update the RDS Security Group inbound rule to allow traffic from the ECS task Security Group ID rather than 0.0.0.0/0. The container connects to the RDS private DNS endpoint instead of the public endpoint, and no traffic leaves the VPC.

Build docs developers (and LLMs) love