CS 3550 deployment guide

From localhost to a public ASP.NET Core app

This guide turns the Lecture 5 AWS/EC2 walkthrough and the Lecture 6 production review into one deployable path for an existing ASP.NET Core MVC project.

High-level overview

Your development computer builds the application. An Ubuntu EC2 instance runs the published files. The ASP.NET Core Kestrel server listens only on the instance's loopback address, while Nginx acts as the public front door. DNS gives the instance a memorable name, and a Let's Encrypt certificate protects browser traffic with HTTPS. Finally, systemd starts the app at boot and restarts it after a failure.

Request path from a browser through DNS, EC2, Nginx with HTTPS, Kestrel, and the ASP.NET Core MVC application.
Figure 1. The public request path. Nginx accepts encrypted traffic on port 443 and forwards it to Kestrel on the server's private loopback interface.
  1. Browser
  2. DNS
  3. EC2 security group
  4. Nginx + TLS
  5. Kestrel on 127.0.0.1:5000
  6. MVC response

The important security boundary is simple: expose only ports 80 and 443 to the web, restrict port 22 to your IP when practical, and do not expose Kestrel's port 5000 publicly.

Condensed walkthrough

  1. Launch Ubuntu on EC2. Create or download an SSH key pair and attach a security group that permits SSH, HTTP, and HTTPS. See Appendix B.
  2. Give the server a stable address. Allocate an Elastic IP, associate it with the instance, and point your domain's DNS records to it. See Appendix C.
  3. Prepare Ubuntu. Connect with SSH, install the matching .NET runtime and Nginx, and create the application directory. See Appendix D.
  4. Publish locally and copy the result. Run dotnet publish in your VS Code project, then transfer only the publish output with rsync or scp. See Appendix E.
  5. Keep the app running. Define and enable a systemd service that launches your DLL on 127.0.0.1:5000. See Appendix F.
  6. Put Nginx in front. Proxy requests from your domain on port 80 to Kestrel and verify the Nginx configuration. See Appendix G.
  7. Turn on HTTPS. After DNS and HTTP work, use Certbot's Nginx integration and test certificate renewal. See Appendix H.
  8. Verify the public result. Visit both the HTTP and HTTPS URLs. HTTP should redirect to HTTPS, and the MVC app should load over a valid certificate.

Appendix A: values to choose first

Replace every brace-delimited placeholder below with your own value. Linux paths and filenames are case-sensitive.

PlaceholderExampleMeaning
{APP_NAME}SurveySaysProject name and published DLL name.
{SERVICE_NAME}surveysaysLowercase systemd service name.
{DOMAIN}example.orgYour registered domain without www.
{ELASTIC_IP}203.0.113.10The static public IPv4 address associated with EC2.
{KEY_FILE}cs3550.pemYour downloaded private key on the development computer.
{PROJECT_FILE}SurveySays.csprojThe project to publish from your repository.

Appendix B: launch the EC2 instance

  1. Enter the AWS Academy Learner Lab, start the lab, wait for its status indicator to turn green, and open the AWS console. In a regular AWS account, open the EC2 console directly.
  2. Choose Launch instance. Give the instance a recognizable name and select the Ubuntu Server LTS image required by the course. The Lecture 5 example uses Ubuntu Server 26.04 LTS, a t3.large instance, and 40 GiB of gp3 storage; use the exact course requirement if it differs.
  3. Create an ED25519 key pair in PEM format. Download the file once and store it somewhere private. AWS cannot give you the private key again.
  4. Create a security group with these inbound rules:
TypePortSourcePurpose
SSH22My IP, when practicalAdministrative shell access.
HTTP80Anywhere IPv4/IPv6Public web traffic and certificate validation.
HTTPS443Anywhere IPv4/IPv6Encrypted public web traffic.
Security boundary showing public HTTP and HTTPS, SSH restricted to the student's IP, Nginx as the public front door, and Kestrel restricted to localhost.
Figure 2. Expose only what needs to be public. Internet traffic reaches Nginx on ports 80 and 443; administrative SSH is restricted, and Kestrel stays private.

Launch the instance, wait until it is running and its status checks pass, then record the instance ID and public address.

Appendix C: Elastic IP and DNS

  1. In EC2, open Network & Security → Elastic IP addresses, allocate an address in the same AWS region as the instance, and associate it with the instance.
  2. At your domain registrar or DNS provider, create an A record with host/name @ and value {ELASTIC_IP}.
  3. Create a CNAME record with host/name www and value {DOMAIN}. Some providers require a trailing period; follow that provider's UI.
  4. Wait for DNS propagation. A 30-minute TTL does not guarantee a 30-minute update; caches can make the change take longer.
A domain's DNS A record points to a stable Elastic IP associated with an EC2 instance.
Figure 3. DNS maps the domain to the Elastic IP, and AWS associates that stable public address with the EC2 instance.
# Run on your development computer to check DNS:
dig +short {DOMAIN}
dig +short www.{DOMAIN}
Checkpoint: both names should ultimately resolve to {ELASTIC_IP}. Do not request the certificate until they do.

Appendix D: SSH and server software

1. Protect the key and connect

# Run on macOS/Linux in the folder containing the key:
chmod 400 {KEY_FILE}
ssh -i {KEY_FILE} ubuntu@{ELASTIC_IP}

On Windows, use the OpenSSH client in PowerShell and protect the PEM file using Windows file permissions. The login name for the official Ubuntu AMI is normally ubuntu.

2. Update Ubuntu and install Nginx

sudo apt update
sudo apt upgrade -y
sudo apt install -y nginx
sudo systemctl enable --now nginx

3. Install .NET

Install the runtime that matches your project's target framework. Lecture 5 uses .NET 10:

# Runtime is sufficient to host a pre-published app.
sudo apt install -y aspnetcore-runtime-10.0

# Verify the installation.
dotnet --info
dotnet --list-runtimes

If the Ubuntu image does not offer that package, use Microsoft's Ubuntu installation instructions for your exact Ubuntu and .NET versions. Installing the SDK (dotnet-sdk-10.0) also works, but a production server does not need compiler tooling when you publish locally.

4. Create the deployment directory

sudo mkdir -p /var/www/{APP_NAME}/publish
sudo chown -R ubuntu:www-data /var/www/{APP_NAME}
sudo chmod -R u=rwX,g=rX,o= /var/www/{APP_NAME}
sudo chmod g+s /var/www/{APP_NAME}

Appendix E: publish and transfer your app

1. Publish from the VS Code project

Open VS Code's integrated terminal at the solution or project directory on your development computer:

dotnet restore
dotnet publish {PROJECT_FILE} -c Release -o ./publish

# Confirm the application assembly was produced.
ls -l ./publish/{APP_NAME}.dll

This is the key Lecture 6 transition: build on the development computer, copy the publish output to EC2, and run that output in production.

Deployment pipeline from an MVC project through dotnet publish and secure file transfer to the Linux publish directory and a systemd service.
Figure 4. Build locally, transfer the publish output securely, and let systemd run the application as a managed Linux service.

2. Copy the files

rsync is best for repeated deployments because it copies only changes. The trailing slashes matter:

rsync -av --delete \
  -e "ssh -i {KEY_FILE}" \
  ./publish/ ubuntu@{ELASTIC_IP}:/var/www/{APP_NAME}/publish/

For a one-time copy, use scp:

scp -i {KEY_FILE} -r ./publish/. \
  ubuntu@{ELASTIC_IP}:/var/www/{APP_NAME}/publish/

3. Confirm the server received the DLL

# Run on EC2:
ls -l /var/www/{APP_NAME}/publish/{APP_NAME}.dll

Appendix F: run the app with systemd

Create the service file on EC2:

sudo nano /etc/systemd/system/{SERVICE_NAME}.service

Paste this definition, replacing the placeholders:

[Unit]
Description={APP_NAME} ASP.NET Core application
After=network.target

[Service]
WorkingDirectory=/var/www/{APP_NAME}/publish
ExecStart=/usr/bin/dotnet /var/www/{APP_NAME}/publish/{APP_NAME}.dll
User=www-data
Group=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://127.0.0.1:5000
Restart=always
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier={SERVICE_NAME}

[Install]
WantedBy=multi-user.target

Save, reload systemd, enable the service at boot, and start it:

sudo systemctl daemon-reload
sudo systemctl enable --now {SERVICE_NAME}
sudo systemctl status {SERVICE_NAME} --no-pager

# Test Kestrel from inside EC2.
curl -I http://127.0.0.1:5000
Checkpoint: systemd should report active (running), and the local curl should return an HTTP response. A redirect is acceptable.

If the service fails, inspect the current boot's logs:

sudo journalctl -u {SERVICE_NAME} -b --no-pager -n 100

Appendix G: configure Nginx

Configure HTTP first. This makes the site testable and gives Certbot a working Nginx site to upgrade.

sudo nano /etc/nginx/sites-available/default

Replace the file's server block with:

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    server_name {DOMAIN} www.{DOMAIN};

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Validate before reloading. Never reload a configuration that fails the syntax test.

sudo nginx -t
sudo systemctl reload nginx
Checkpoint: visit http://{DOMAIN}. Your MVC app—not the default “Welcome to nginx” page—should appear.

Appendix H: add HTTPS

Continue only after both domain names resolve to the Elastic IP and the HTTP checkpoint succeeds.

sudo snap install --classic certbot

# Create the command link only if `certbot` is not already found.
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot

sudo certbot --nginx -d {DOMAIN} -d www.{DOMAIN}

When prompted, choose the HTTPS redirect. Certbot obtains the certificate and edits the working Nginx configuration. Then verify the configuration and renewal process:

sudo nginx -t
sudo systemctl reload nginx
sudo certbot renew --dry-run
Final checkpoint: https://{DOMAIN} and https://www.{DOMAIN} should load without a certificate warning, and http://{DOMAIN} should redirect to HTTPS.

Appendix I: deploy an update

For a simple class deployment, use this repeatable cycle from the development computer:

dotnet publish {PROJECT_FILE} -c Release -o ./publish

rsync -av --delete \
  -e "ssh -i {KEY_FILE}" \
  ./publish/ ubuntu@{ELASTIC_IP}:/var/www/{APP_NAME}/publish/

ssh -i {KEY_FILE} ubuntu@{ELASTIC_IP} \
  "sudo systemctl restart {SERVICE_NAME} && sudo systemctl status {SERVICE_NAME} --no-pager"

Test a real route—not only the home page—after every update. For a larger or higher-stakes application, add backups, database migration planning, a staging directory, health checks, and an automated deployment pipeline.

Appendix J: troubleshoot and clean up

Four troubleshooting checkpoints: DNS resolution, systemd application status, Kestrel response on localhost, and Nginx configuration validity.
Figure 5. Diagnose one layer at a time. Start with DNS, then verify the service, Kestrel, and Nginx in order.
SymptomLikely layerWhat to check
SSH times outAWS/networkInstance is running; port 22 permits your current IP; key and username are correct.
Domain does not resolveDNSA record uses the Elastic IP; www CNAME targets the root domain; allow propagation time.
502 Bad GatewayKestrel/systemdsystemctl status, journalctl, DLL name, runtime version, and port 5000.
Default Nginx pageNginxCorrect site file, server_name, successful nginx -t, and reload.
Certbot validation failsDNS/firewall/HTTPBoth names resolve correctly; ports 80 and 443 are open; HTTP works before Certbot.
Site works until SSH closesProcess managementRun through systemd, not a foreground dotnet process in your shell.

Useful checks

sudo systemctl status {SERVICE_NAME} --no-pager
sudo journalctl -u {SERVICE_NAME} -b --no-pager -n 100
sudo nginx -t
sudo tail -n 100 /var/log/nginx/error.log
curl -I http://127.0.0.1:5000
curl -I http://{DOMAIN}
curl -I https://{DOMAIN}

Stop unnecessary charges

When the project ends, stop or terminate the instance as appropriate and release an Elastic IP you no longer need. Stopped instances can still incur storage and public IPv4-related charges. In AWS Academy, remember that the public site is reachable only while the learner lab resources are running.

Lecture basis and further reading

This article condenses CS 3550 Lecture 5, “Deployment — AWS and EC2s,” and the deployment portion of Lecture 6, “Forms/Deployment/Work Time.” It adapts the lecture's sample app into a placeholder-based workflow for an existing VS Code project and orders HTTP configuration before Certbot so each layer can be tested.

AWS console labels, available instance images, course requirements, and software package versions can change. When the course's current instructions differ, use the assigned values while keeping the architecture and verification checkpoints in this guide.