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.
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.
- Browser
- DNS
- EC2 security group
- Nginx + TLS
- Kestrel on 127.0.0.1:5000
- 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
- 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.
- 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.
- Prepare Ubuntu. Connect with SSH, install the matching .NET runtime and Nginx, and create the application directory. See Appendix D.
- 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.
- Keep the app running. Define and enable a systemd service that launches your DLL on
127.0.0.1:5000. See Appendix F.
- Put Nginx in front. Proxy requests from your domain on port 80 to Kestrel and verify the Nginx configuration. See Appendix G.
- Turn on HTTPS. After DNS and HTTP work, use Certbot's Nginx integration and test certificate renewal. See Appendix H.
- 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.
| Placeholder | Example | Meaning |
{APP_NAME} | SurveySays | Project name and published DLL name. |
{SERVICE_NAME} | surveysays | Lowercase systemd service name. |
{DOMAIN} | example.org | Your registered domain without www. |
{ELASTIC_IP} | 203.0.113.10 | The static public IPv4 address associated with EC2. |
{KEY_FILE} | cs3550.pem | Your downloaded private key on the development computer. |
{PROJECT_FILE} | SurveySays.csproj | The project to publish from your repository. |
Back to contents
Appendix B: launch the EC2 instance
- 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.
- 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.
- 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.
- Create a security group with these inbound rules:
| Type | Port | Source | Purpose |
| SSH | 22 | My IP, when practical | Administrative shell access. |
| HTTP | 80 | Anywhere IPv4/IPv6 | Public web traffic and certificate validation. |
| HTTPS | 443 | Anywhere IPv4/IPv6 | Encrypted public web traffic. |
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.
Back to contents
Appendix C: Elastic IP and DNS
- 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.
- At your domain registrar or DNS provider, create an A record with host/name
@ and value {ELASTIC_IP}.
- Create a CNAME record with host/name
www and value {DOMAIN}. Some providers require a trailing period; follow that provider's UI.
- Wait for DNS propagation. A 30-minute TTL does not guarantee a 30-minute update; caches can make the change take longer.
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.
Back to contents
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}
Back to contents
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.
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
Back to contents
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
Back to contents
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.
Back to contents
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.
Back to contents
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.
Back to contents
Appendix J: troubleshoot and clean up
Figure 5. Diagnose one layer at a time. Start with DNS, then verify the service, Kestrel, and Nginx in order.
| Symptom | Likely layer | What to check |
| SSH times out | AWS/network | Instance is running; port 22 permits your current IP; key and username are correct. |
| Domain does not resolve | DNS | A record uses the Elastic IP; www CNAME targets the root domain; allow propagation time. |
| 502 Bad Gateway | Kestrel/systemd | systemctl status, journalctl, DLL name, runtime version, and port 5000. |
| Default Nginx page | Nginx | Correct site file, server_name, successful nginx -t, and reload. |
| Certbot validation fails | DNS/firewall/HTTP | Both names resolve correctly; ports 80 and 443 are open; HTTP works before Certbot. |
| Site works until SSH closes | Process management | Run 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.
Back to contents
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.