Deployment Lesson: Getting Your Project on the Internet
A full class lesson on deploying a team frontend and backend for Open Coding Society, covering domains, DNS, IPs, ports, Docker, nginx, HTTPS, and Certbot
Why this lesson matters
Right now your project probably works on localhost, which means only your laptop can see it. Deployment is the process of putting it on the internet so anyone, anywhere can type a web address and use it.
By the end of this lesson you should be able to:
- Explain what happens between typing
prc.opencodingsociety.comand seeing a page. - Define domain, subdomain, DNS, public IP, private IP, port, HTTP/HTTPS and certificate.
- Change your team’s backend to use its own port.
- Run your backend on the server with Docker.
- Connect it to the internet with nginx and HTTPS.
- Point your frontend at your deployed backend.
- Fix the most common deployment errors.
Part 1: Vocabulary
| Term | What it means for us |
|---|---|
| AWS | Amazon’s cloud. Holds everything below |
| EC2 | The actual server (a computer in AWS) that runs all the backends |
| Domain | opencodingsociety.com. The main root web address we own for the organization |
| Subdomain | A name under the main domain that belongs to one team (e.g. prc.opencodingsociety.com = Poway Recovery Center frontend, mgb-api.opencodingsociety.com = MGB’s backend) |
| DNS | Domain Name System. Turns a name into an IP address. For us it’s AWS Route 53, where CNAME and A records point subdomains to GitHub Pages or the AWS server |
| Hosted zone | The folder of records for one domain (opencodingsociety.com) |
| Record | One entry in the hosted zone |
| CNAME record | Name → another name. Used for frontends (prc → poway-recovery-center.github.io) |
| A record | Name → IP address. Used for backends (prc-api → 3.129.109.200) |
| Public IP | 3.129.109.200. The exact public address of our EC2 server. Anyone on the internet can reach it |
| Private IP / Port | Inside the server, each team’s backend runs on its own port (e.g. 8450 for PRC, 8451 for MGB). The internet can’t reach 8450 directly; nginx receives the request on port 443 and forwards it to that port |
| Docker | Packages your backend and everything it needs into a container so it runs the same on any computer |
| nginx | Web server. Listens on ports 80 and 443 and forwards name-api.opencodingsociety.com → the right port |
| HTTP vs HTTPS | HTTP = normal, unencrypted. HTTPS = encrypted and secure. GitHub Pages forces frontends to use HTTPS, so backends must use HTTPS too or the browser blocks the connection |
| Certificate | The digital ID made when you run sudo certbot --nginx. It’s what gives prc-api.opencodingsociety.com its lock icon |
| Certbot | The tool that creates the certificate and updates nginx for HTTPS |
| Cockpit | A website (cockpit.stu.opencodingsociety.com) that gives you a terminal on the server |
| Frontend | The website people see (GitHub Pages) |
| Backend | The Flask API that stores and serves data (runs on the server) |
Part 2: How a request works
Here is what happens when someone uses your site.
FRONTEND
Browser types prc.opencodingsociety.com
→ Route 53 (CNAME) says "that's poway-recovery-center.github.io"
→ GitHub Pages sends back the HTML/CSS/JS
BACKEND (when the page calls the API)
JavaScript fetches https://prc-api.opencodingsociety.com/api/...
→ Route 53 (A record) says "that's 3.129.109.200"
→ EC2 server, port 443 (HTTPS)
→ nginx reads the name "prc-api..." and looks up its config
→ proxy_pass to localhost:8450 (private, inside the server)
→ Docker container → Flask → response goes all the way back
Key point: frontend and backend are two separate deployments with two separate subdomains. The frontend lives on GitHub; the backend lives on AWS.
Naming pattern
| Piece | Pattern | PRC example |
|---|---|---|
| Frontend | teamname.opencodingsociety.com |
prc.opencodingsociety.com |
| Backend | teamname-api.opencodingsociety.com |
prc-api.opencodingsociety.com |
| Port | a free 8xxx number |
8450 |
Part 3: DNS records (done by the admin)
Only the admin can make Route 53 records, but you should understand what gets made for your team. Two records per team:
| Frontend record | Backend record | |
|---|---|---|
| Record name | teamname |
teamname-api |
| Type | CNAME | A |
| Value | yourgithuborg.github.io |
3.129.109.200 |
| TTL | 300 | 300 |
Why CNAME for the frontend? GitHub Pages’ IP addresses can change, so we point at GitHub’s name instead. Why A for the backend? Our server has one fixed public IP, so we point straight at it.
You can check that your records work from any terminal:
nslookup teamname.opencodingsociety.com
nslookup teamname-api.opencodingsociety.com # should show 3.129.109.200
New records can take a few minutes to show up (that’s what TTL controls).
Part 4: Ports, and why every team needs its own
One server, many teams. If two backends try to use the same port, the second one fails with “port is already allocated”. So every team picks its own.
How to pick a free port
- In the Cockpit terminal, run
docker ps - Look at the PORTS column (the number before
->) - Pick any
8xxxnumber not in that list
Ports to avoid
- 8587: the starter-code default, so everyone has it
- 8500: already taken for sockets
- Image names
flask_open/socket_openare also taken, so rename them (flask_teamname)
Part 5: Prepare your backend repo (on your laptop)
Change the starter port to your new port in two files.
docker-compose.yml
ports:
- "8587:8587" → - "NEWPORT:NEWPORT"
image: flask_open → image: flask_teamname
If you have a socket service:
- "8500:8500" → - "FREEPORT:8500" # only the LEFT number changes
The left number is the port on the server; the right number is the port inside the container. Only the left side has to be unique across teams.
Dockerfile
--bind=0.0.0.0:8587 → --bind=0.0.0.0:NEWPORT
EXPOSE 8587 → EXPOSE NEWPORT
Test it locally before touching the server:
docker-compose up
Then open http://localhost:NEWPORT. If it doesn’t work on your laptop, it won’t work on the server. Commit and push.
Part 6: Run your backend on the server
Log into Cockpit (cockpit.stu.opencodingsociety.com) and open the terminal.
Look around first (these commands are safe and read-only)
| Command | What it does |
|---|---|
ls |
List folders (each one is a team’s backend) |
cd foldername |
Go into a folder |
cat filename |
Show a file |
docker ps |
Show running backends and their ports |
Clone and start
cd ~
git clone https://github.com/ORG/REPO.git teamname_backend
cd teamname_backend
nano .env # add your settings; save with Ctrl+X → Y → Enter
docker-compose up -d --build # build and start in the background (-d)
curl localhost:NEWPORT # test it from inside the server
If curl returns HTML or JSON, your backend is running privately. It is not on the internet yet. That’s nginx’s job.
Updating later
cd ~/teamname_backend
git pull
docker-compose up -d --build
Part 7: nginx
Create a config file that tells nginx: “when someone asks for teamname-api.opencodingsociety.com, send them to localhost:NEWPORT.”
sudo nano /etc/nginx/sites-available/teamname-api
Paste this and change the two marked lines:
server {
listen 80;
listen [::]:80;
server_name teamname-api.opencodingsociety.com; # ← change
location / {
proxy_pass http://localhost:NEWPORT; # ← change
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;
}
}
Turn it on, test, then reload:
sudo ln -s /etc/nginx/sites-available/teamname-api /etc/nginx/sites-enabled
sudo nginx -t # MUST say "syntax is ok" and "test is successful"
sudo systemctl reload nginx # ONLY if the test passed
⚠️ nginx serves every team. A broken config + reload can take down everyone’s site. Always run
sudo nginx -tfirst.
Now http://teamname-api.opencodingsociety.com should work, but only over plain HTTP.
Part 8: HTTPS and the certificate
Your frontend is on GitHub Pages, which is always HTTPS. Browsers refuse to let an HTTPS page call an HTTP backend (a “mixed content” error). So the backend needs a certificate.
sudo certbot --nginx
Certbot lists every domain on the server. Pick only your team’s number. It will:
- Prove to Let’s Encrypt that you control
teamname-api.opencodingsociety.com(this only works if the DNS A record already points here) - Create the certificate
- Edit your nginx file to listen on 443 and redirect HTTP → HTTPS
Visit https://teamname-api.opencodingsociety.com and look for the lock icon. 🔒
Part 9: Connect the frontend
- GitHub repo → Settings → Pages
- Source: GitHub Actions
- Custom domain:
teamname.opencodingsociety.com
- In
_config.yml, if the site looks broken (no styles, broken links):baseurl: "/portfolio" → baseurl: ""(On a custom domain, the site lives at the root, not under
/portfolio.) - In your frontend code, change the backend URL:
http://localhost:8587 → https://teamname-api.opencodingsociety.com
Push, wait for the Action to finish, and visit https://teamname.opencodingsociety.com.
Part 10: Worked example (PRC)
| Step | PRC value |
|---|---|
| Frontend record | prc CNAME → poway-recovery-center.github.io |
| Backend record | prc-api A → 3.129.109.200 |
| Port | 8450 |
| Server folder | ~/prc_backend |
| nginx file | /etc/nginx/sites-available/prc-api with proxy_pass http://localhost:8450; |
| Certificate | sudo certbot --nginx → picked only prc-api.opencodingsociety.com |
| Frontend URL | https://prc.opencodingsociety.com |
| Backend URL in frontend code | https://prc-api.opencodingsociety.com |
Part 11: Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
nslookup finds nothing |
Record missing or still spreading | Wait a few minutes; ask admin to check Route 53 |
port is already allocated |
Another team uses your port | docker ps, pick a free port, update both files |
curl localhost:NEWPORT fails |
Container not running or crashed | docker ps; docker-compose logs; check .env |
| 502 Bad Gateway | nginx works but nothing is on that port | Container down, or proxy_pass port is wrong |
nginx -t fails |
Typo in your config (missing ; or }) |
Fix the line it names. Don’t reload |
| Certbot fails | DNS doesn’t point to the server yet | Check the A record with nslookup, then retry |
| Browser: “mixed content” / blocked | Frontend calls http:// backend |
Run Certbot and use https:// in your code |
| Browser: CORS error | Backend doesn’t allow your frontend’s domain | Add https://teamname.opencodingsociety.com to your Flask CORS allowed origins |
| Site has no styles / broken links | Wrong baseurl |
Set baseurl: "" in _config.yml |
| Old code still showing | Server didn’t rebuild | git pull then docker-compose up -d --build |
Part 12: Rules
- Every team gets its own port. Check
docker psbefore choosing. - Always
sudo nginx -tbefore reloading nginx. - In Route 53: only create, never edit or delete other teams’ records.
- In Certbot: only pick your own domain.
- Only touch your own folder and your own nginx file on the server.
- Test locally first. If it fails on your laptop, it will fail on the server.
Check your understanding
1. What's the difference between a domain and a subdomain?
The domain is the root address we own (opencodingsociety.com). A subdomain adds a prefix to give one team its own address (prc.opencodingsociety.com).
2. Why is the frontend a CNAME but the backend an A record?
The frontend points at another name (GitHub Pages, whose IPs can change). The backend points straight at our server's fixed public IP, 3.129.109.200.
3. Can someone on the internet visit 3.129.109.200:8450 directly? Why does that matter?
No. 8450 is private inside the server. The public only reaches nginx on ports 80/443, and nginx passes the request privately to 8450. This is why you need an nginx config.
4. In "8452:8500", which number must be unique and why?
The left one (8452). It's the port on the shared server. The right one is inside your own container, so it can be the same as other teams'.
5. Your frontend loads but every API call fails with a "mixed content" error. What's wrong?
The frontend is HTTPS (GitHub Pages) but it's calling an http:// backend. Run sudo certbot --nginx for your API domain and change the URL in your code to https://.
6. You get a 502 Bad Gateway. Is the problem DNS, nginx, or Docker?
DNS and nginx are working (you reached nginx). nginx can't reach your app, so the container is down or proxy_pass has the wrong port. Check docker ps.
7. Why must you run sudo nginx -t before reloading?
nginx is shared by every team. Reloading a broken config can take every team's backend offline.
Your deployment checklist
- Picked a free port (checked
docker ps) - Updated
docker-compose.yml(port + image names) - Updated
Dockerfile(--bind+EXPOSE) docker-compose upworks on my laptop- Admin made my
teamnameandteamname-apirecords - Cloned repo on the server, added
.env, randocker-compose up -d --build curl localhost:NEWPORTworks on the server- Made nginx file,
nginx -tpassed, reloaded - Ran
sudo certbot --nginxfor only my domain https://teamname-api.opencodingsociety.comshows a lock- GitHub Pages custom domain set,
baseurlfixed - Frontend code uses
https://teamname-api.opencodingsociety.com - Full site works at
https://teamname.opencodingsociety.com🎉