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.

Five phases of deployment A vertical pipeline diagram showing the five phases of deploying an ASP.NET Core application to production: (1) Register the domain with a registrar such as SquareSpace, (2) Provision the AWS EC2 instance with a stable Elastic IP and open ports, (3) Install server software including Nginx and the .NET runtime, (4) Publish the app locally with dotnet publish and transfer the output to the server using rsync, and (5) Wire everything together with systemd, Nginx as a reverse proxy, and Let's Encrypt for HTTPS. 1. Register the Domain Registrar (SquareSpace) — buy name, set DNS records Result: mydomain.org → (soon) an Elastic IP 2. Provision the Server AWS EC2 — launch instance, allocate Elastic IP Result: an Ubuntu Linux box at 203.0.113.42 3. Prepare the Server Install Nginx, the .NET runtime, and supporting tools Result: a Linux box ready to run web software 4. Publish + Transfer the App dotnet publish on the dev machine, rsync to /var/www Result: SurveySays.dll and dependencies on the server 5. Wire Everything Together systemd runs the app; Nginx proxies; Certbot enables HTTPS Result: HTTPS traffic reaches the ASP.NET Core app Live at https://mydomain.org Publicly reachable, encrypted, auto-restarting Auto-renewing HTTPS certificate
The five phases of the deployment pipeline, from registering a domain to a live HTTPS service.
  1. Register the domain — purchase a human-readable name from a registrar (SquareSpace) and configure DNS records that will point at the production server.
  2. 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.
  3. Prepare the server — install the Nginx web server, the .NET runtime, and any supporting tools using the Ubuntu package manager.
  4. Publish and transfer the application — compile the app on the developer's machine with dotnet publish, then copy the compiled output to the server with rsync.
  5. 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.

Development machine to production server file transfer A diagram showing the developer's local machine on the left containing the source code checked out from Git and a publish folder produced by dotnet publish. An arrow labeled "rsync over SSH with --delete" crosses the middle to the production server on the right, where the transferred files land in /var/www/SurveySays and are executed by Kestrel behind Nginx. Developer's Machine (Mac) Git Repository Controllers, Views, Models, .csproj SurveySays/ publish/ (dotnet publish -c Release) SurveySays.dll SurveySays.deps.json appsettings.json web.config wwwroot/ Production Server (AWS EC2) Nginx :443 TLS termination, reverse proxy /etc/nginx/sites-available/default /var/www/SurveySays/ SurveySays.dll appsettings.json wwwroot/ Owned by ubuntu:www-data Run by systemd (Kestrel :5000) rsync -avz --delete over SSH
The developer's compiled output is transferred to the server's application directory with a single rsync command.

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.

  1. Register a domain (e.g., mydomain.org) with a domain registrar. See Appendix A.
  2. Launch an Ubuntu EC2 instance in AWS Academy Learner Lab. See Appendix B.
  3. Create a security group that opens ports 22 (SSH), 80 (HTTP), and 443 (HTTPS). See Appendix B.
  4. Allocate an Elastic IP and associate it with the instance. See Appendix B.
  5. At the registrar, create A records pointing mydomain.org and www.mydomain.org at the Elastic IP. See Appendix A.
  6. SSH into the instance: $ ssh -i keyfile.pem ubuntu@203.0.113.42. See Appendix B.
  7. Update packages: # sudo apt update && sudo apt upgrade -y. See Appendix C.
  8. Install Nginx: # sudo apt install -y nginx. See Appendix C.
  9. Install the ASP.NET Core runtime: # sudo apt install -y aspnetcore-runtime-10.0. See Appendix C.
  10. Create the application directory /var/www/SurveySays with proper ownership. See Appendix D.
  11. On the developer's machine, compile the application: $ dotnet publish -c Release -o ./publish. See Appendix D.
  12. Transfer the compiled output to the server with rsync. See Appendix D.
  13. Create a systemd service file at /etc/systemd/system/SurveySays.service. See Appendix E.
  14. Enable and start the service: # sudo systemctl enable --now SurveySays. See Appendix E.
  15. Edit the Nginx site configuration to add the domain name and forward requests to Kestrel. See Appendix F.
  16. Install Certbot and request an HTTPS certificate. See Appendix G.
  17. Reload Nginx: # sudo systemctl reload nginx. See Appendix G.
  18. 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 (@ or mydomain.org) and one for www, both pointing to the Elastic IP.
  • CNAME record — aliases one name to another. Some registrars use a CNAME on www that 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:

  1. Open the AWS Academy Learner Lab and click "Start Lab." When the lab is ready, click "AWS" to enter the console.
  2. Navigate to EC2 → Instances → Launch Instance.
  3. Choose Ubuntu as the AMI (Amazon Machine Image).
  4. Choose a small instance type such as t2.micro or t3.micro.
  5. Select a key pair (AWS Academy provides vockey by default; a matching labsuser.pem private key file is downloadable from the "AWS Details" panel). Save the .pem file—it cannot be recovered later.
  6. Create or select a security group that opens three inbound ports to 0.0.0.0/0: SSH (22), HTTP (80), and HTTPS (443).
  7. Launch the instance.

After launch, allocate an Elastic IP so the instance has a stable public address:

  1. EC2 → Elastic IPs → Allocate Elastic IP address.
  2. 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:

  1. Proves ownership of the domain by responding to an HTTP challenge on port 80.
  2. Downloads the signed certificate and its private key.
  3. Modifies the Nginx configuration to add ssl_certificate, ssl_certificate_key, and related directives that reference the new certificate.
  4. Offers to redirect all HTTP traffic to HTTPS—select option 2 (Redirect).
  5. 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 +short returns the Elastic IP.
  • SSH is reachable. nc -zv 203.0.113.42 22 reports success.
  • The service is running. sudo systemctl status SurveySays --no-pager shows Active: active (running).
  • Kestrel is answering internally. curl http://127.0.0.1:5000 returns HTML.
  • Nginx configuration is valid. sudo nginx -t reports "syntax is ok" and "test is successful."
  • HTTPS works. Visiting https://mydomain.org in a browser loads the application and shows a padlock in the address bar.
  • HTTP redirects to HTTPS. Visiting http://mydomain.org automatically forwards to https://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.