Overview of Publishing Steps (Dev → Deployed)
This article summarizes the steps required to take an ASP.NET Core MVC application from a local development machine to a publicly reachable HTTPS web service running on AWS EC2. It begins with a high-level overview, followed by a condensed walkthrough, and finishes with detailed appendices—one per phase—reachable via in-page anchor links. For a deeper explanation of how a browser request travels through the deployed system, see How a Web Request Reaches an ASP.NET Core Application.
Throughout this article the domain mydomain.org stands in for whichever
team member's domain a reader is deploying, and 203.0.113.42 (from the
IANA RFC 5737 documentation range) stands in for a real Elastic IP.
The Big Picture (High-Level Overview)
Deploying a web application is not a single command—it is a sequence of five distinct phases, each handled by a different tool. This layered approach is what separates "runs on my laptop" from a real internet service.
- Register the domain — purchase a human-readable name from a registrar (SquareSpace) and configure DNS records that will point at the production server.
- Provision the server — launch an AWS EC2 Linux instance, allocate an Elastic IP so the public address is stable, and open the required network ports.
- Prepare the server — install the Nginx web server, the .NET runtime, and any supporting tools using the Ubuntu package manager.
- Publish and transfer the application — compile the app on the developer's machine with
dotnet publish, then copy the compiled output to the server withrsync. - Wire everything together — run the app as a Linux service, put Nginx in front of it as a reverse proxy, and enable HTTPS with a certificate from Let's Encrypt.
Two very different environments are involved: the developer's local machine (where the code lives and is compiled) and the production server (where the compiled binaries are executed and made available on the internet). The diagram below sketches how the two connect during a deploy.
Condensed Walkthrough
The following list summarizes the entire deployment as a numbered sequence. Each
item links to the appendix that explains it in detail. Commands prefixed with
$ run on the developer's local machine; commands prefixed with
# run on the remote EC2 server.
- Register a domain (e.g.,
mydomain.org) with a domain registrar. See Appendix A. - Launch an Ubuntu EC2 instance in AWS Academy Learner Lab. See Appendix B.
- Create a security group that opens ports 22 (SSH), 80 (HTTP), and 443 (HTTPS). See Appendix B.
- Allocate an Elastic IP and associate it with the instance. See Appendix B.
- At the registrar, create A records pointing
mydomain.organdwww.mydomain.orgat the Elastic IP. See Appendix A. - SSH into the instance:
$ ssh -i keyfile.pem ubuntu@203.0.113.42. See Appendix B. - Update packages:
# sudo apt update && sudo apt upgrade -y. See Appendix C. - Install Nginx:
# sudo apt install -y nginx. See Appendix C. - Install the ASP.NET Core runtime:
# sudo apt install -y aspnetcore-runtime-10.0. See Appendix C. - Create the application directory
/var/www/SurveySayswith proper ownership. See Appendix D. - On the developer's machine, compile the application:
$ dotnet publish -c Release -o ./publish. See Appendix D. - Transfer the compiled output to the server with rsync. See Appendix D.
- Create a systemd service file at
/etc/systemd/system/SurveySays.service. See Appendix E. - Enable and start the service:
# sudo systemctl enable --now SurveySays. See Appendix E. - Edit the Nginx site configuration to add the domain name and forward requests to Kestrel. See Appendix F.
- Install Certbot and request an HTTPS certificate. See Appendix G.
- Reload Nginx:
# sudo systemctl reload nginx. See Appendix G. - Verify the site at
https://mydomain.org. See Appendix H.
Appendix A — Domain Registration and DNS
A domain name is purchased from a domain registrar. This deployment uses SquareSpace, though any registrar works. After purchase, the registrar's DNS configuration panel is where records are added that map a name to a numeric address.
Two record types are relevant:
- A record — maps a name directly to an IPv4 address. Two are typically added: one for the root domain (
@ormydomain.org) and one forwww, both pointing to the Elastic IP. - CNAME record — aliases one name to another. Some registrars use a CNAME on
wwwthat points back to the root domain instead of a second A record; either style works.
While testing, a short TTL (time-to-live) such as 300 seconds is convenient because DNS changes propagate quickly. Once the deployment is stable, the TTL can be raised.
After entering the records, DNS propagation can take between five and thirty minutes. The result can be verified from any machine:
dig mydomain.org +short
# Expected output:
# 203.0.113.42
Appendix B — Provisioning the EC2 Instance
An EC2 instance is a virtual Linux machine hosted by AWS. In this course it is provisioned through AWS Academy Learner Lab. The provisioning steps are:
- Open the AWS Academy Learner Lab and click "Start Lab." When the lab is ready, click "AWS" to enter the console.
- Navigate to EC2 → Instances → Launch Instance.
- Choose Ubuntu as the AMI (Amazon Machine Image).
- Choose a small instance type such as
t2.microort3.micro. - Select a key pair (AWS Academy provides
vockeyby default; a matchinglabsuser.pemprivate key file is downloadable from the "AWS Details" panel). Save the.pemfile—it cannot be recovered later. - Create or select a security group that opens three inbound ports to
0.0.0.0/0: SSH (22), HTTP (80), and HTTPS (443). - Launch the instance.
After launch, allocate an Elastic IP so the instance has a stable public address:
- EC2 → Elastic IPs → Allocate Elastic IP address.
- Select the newly allocated address, choose Actions → Associate Elastic IP address, and pick the running instance.
SSH into the instance from a local terminal:
chmod 400 ~/.ssh/labsuser.pem
ssh -i ~/.ssh/labsuser.pem ubuntu@203.0.113.42
The chmod 400 step is required by SSH—the private key file must be
readable only by the owner, or SSH refuses to use it.
Appendix C — Installing Server Software
Once inside the EC2 instance over SSH, refresh the Ubuntu package index and install the two pieces of server software the application needs: Nginx (a web server that will act as a reverse proxy) and the ASP.NET Core runtime (the .NET libraries needed to execute the compiled application).
# sudo apt update && sudo apt upgrade -y
# sudo apt install -y nginx
# sudo apt install -y aspnetcore-runtime-10.0
Verify that Nginx is running:
# sudo systemctl status nginx
# Expect a green "Active: active (running)" line.
Confirm from a browser by visiting http://203.0.113.42 (plain HTTP, no
HTTPS yet)—the "Welcome to nginx!" default page should appear. This proves that the
security group allows public HTTP traffic and that Nginx is responding.
Verify the .NET runtime:
# dotnet --list-runtimes
# Expect both:
# Microsoft.AspNetCore.App 10.0.x
# Microsoft.NETCore.App 10.0.x
Appendix D — Publishing and Transferring the App
The application is built on the developer's machine and then transferred to the server. It is not built on the server itself, because the server does not need the .NET SDK or the source code—only the compiled output and the runtime.
1. Create the application directory on the server
The compiled files will land in /var/www/SurveySays. That directory must
exist and have appropriate ownership before rsync can write to it:
# sudo mkdir -p /var/www/SurveySays
# sudo chown -R ubuntu:www-data /var/www/SurveySays
# sudo chmod -R u=rwX,g=rX,o= /var/www/SurveySays
# sudo chmod g+s /var/www/SurveySays
The ownership ubuntu:www-data lets the deploying user (ubuntu) write
files while the web server user (www-data) can read them. The g+s
setgid bit ensures that new files inherit the www-data group
automatically.
2. Publish the app on the developer's machine
From the project's source folder:
$ dotnet publish -c Release -o ./publish
This produces a publish/ directory containing SurveySays.dll,
its dependency DLLs, appsettings.json, and wwwroot/. Every
file the running application needs is inside publish/.
3. Transfer with rsync
The rsync command copies files over SSH efficiently, transferring only what has changed since the last deploy:
$ rsync -avz --delete -e "ssh -i ~/.ssh/labsuser.pem" ./publish/ ubuntu@203.0.113.42:/var/www/SurveySays/
The important flags:
-a— archive mode (preserves permissions, symlinks, timestamps).-v— verbose (prints what is transferring).-z— compress during transfer.--delete— remove files on the server that no longer exist locally, keeping the two folders identical.-e "ssh -i ..."— tunnel over SSH using a specific private key.
The trailing slash on ./publish/ matters: it means
"copy the contents of publish, not the folder itself." Without the slash, an extra
publish/ subdirectory would be created on the server and the systemd
service file would fail to find the DLL.
Warning: rsync's --delete can wipe files if the command
is broken across two shell commands (for example, by a copy/paste line break). The
command above must be entered as a single line.
Appendix E — Running the App as a systemd Service
systemd is Linux's service manager. It starts, stops, restarts, and monitors background processes. Turning the ASP.NET Core app into a systemd service means it starts automatically when the server boots, restarts automatically if it crashes, and runs as a low-privilege user rather than as root.
Create the service file at /etc/systemd/system/SurveySays.service:
[Unit]
Description=SurveySays ASP.NET Core application
After=network.target
[Service]
WorkingDirectory=/var/www/SurveySays
ExecStart=/usr/bin/dotnet /var/www/SurveySays/SurveySays.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=surveysays
[Install]
WantedBy=multi-user.target
Key parts of this file:
ExecStart— the command systemd runs to launch the app.User=www-data/Group=www-data— the low-privilege identity the process runs under.ASPNETCORE_ENVIRONMENT=Production— switches off developer-friendly error pages that could leak stack traces to attackers.ASPNETCORE_URLS=http://127.0.0.1:5000— tells Kestrel to bind only to the loopback interface. The public internet cannot reach this port directly; only Nginx (running on the same machine) can.Restart=always/RestartSec=10— if the app exits for any reason, systemd waits ten seconds and starts it again.
Load, enable, and start the service:
# sudo systemctl daemon-reload
# sudo systemctl enable --now SurveySays
# sudo systemctl status SurveySays --no-pager
enable --now both enables the service at boot and starts it immediately.
The status command should show Active: active (running). A quick smoke
test confirms Kestrel is answering on its private port:
# curl http://127.0.0.1:5000
# Expected: raw HTML of the app's home page.
Appendix F — Nginx as a Reverse Proxy
Nginx's default configuration on Ubuntu serves static files from /var/www/html
and knows nothing about the ASP.NET Core application. Two edits change that.
Open the default site configuration file for editing:
# sudo nano /etc/nginx/sites-available/default
Edit 1: set the server_name
Find the line that reads server_name _; (or a comment referencing
example.com) and replace it with the deployment's domain:
server_name mydomain.org www.mydomain.org;
Edit 2: replace the location / block
Inside the HTTPS server block, replace the default static-file handler:
location / {
try_files $uri $uri/ =404;
}
with a reverse-proxy handler that forwards requests to Kestrel:
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;
}
The proxy_set_header lines forward useful metadata (client IP address,
whether HTTPS was used) to the application, which would otherwise only see requests
coming from 127.0.0.1 over plain HTTP.
Test the configuration for syntax errors and reload:
# sudo nginx -t
# sudo systemctl reload nginx
Appendix G — HTTPS with Let's Encrypt
Let's Encrypt is a free automated Certificate Authority. Certbot is a small command-line tool that requests, installs, and renews certificates from Let's Encrypt. Installing HTTPS is a matter of installing Certbot and running one command.
# sudo snap install --classic certbot
# sudo ln -s /snap/bin/certbot /usr/bin/certbot
# sudo certbot --nginx -d mydomain.org -d www.mydomain.org
Certbot prompts for an email address (used for expiry notifications) and asks the user to agree to the terms of service. It then:
- Proves ownership of the domain by responding to an HTTP challenge on port 80.
- Downloads the signed certificate and its private key.
- Modifies the Nginx configuration to add
ssl_certificate,ssl_certificate_key, and related directives that reference the new certificate. - Offers to redirect all HTTP traffic to HTTPS—select option 2 (Redirect).
- Schedules a systemd timer that renews the certificate automatically before expiry.
The certificate lasts ninety days and is renewed automatically; no further manual action is required.
Appendix H — Verification Checklist
Once the deployment steps are complete, work through the following checks to confirm each layer is operating correctly.
- DNS resolves.
dig mydomain.org +shortreturns the Elastic IP. - SSH is reachable.
nc -zv 203.0.113.42 22reports success. - The service is running.
sudo systemctl status SurveySays --no-pagershowsActive: active (running). - Kestrel is answering internally.
curl http://127.0.0.1:5000returns HTML. - Nginx configuration is valid.
sudo nginx -treports "syntax is ok" and "test is successful." - HTTPS works. Visiting
https://mydomain.orgin a browser loads the application and shows a padlock in the address bar. - HTTP redirects to HTTPS. Visiting
http://mydomain.orgautomatically forwards tohttps://mydomain.org. - Full navigation works. The nav dropdown items, the encyclopedia articles, and the Demographics form submit successfully over HTTPS.
When each check passes, the deployment is complete: a locally developed ASP.NET Core application is now a live internet service, publicly reachable at a real domain, over HTTPS, with automatic restarts, automatic certificate renewal, and a proper separation of the process running as a low-privilege user behind a hardened reverse proxy.