Gunicorn instead of runserver
Gunicorn is a WSGI server. It loads your Django project and answers HTTP with several worker processes. runserver is a single process that Django ships for development. It reloads code, it is not built for concurrent users, and it should not be the process on a public port.
gunicorn project.wsgi:application --bind 0.0.0.0:8000 --workers 3
project.wsgi:application is the callable Django generated. --bind 0.0.0.0:8000 listens on every interface of the machine, on port 8000. Inside Docker that is the container’s port. On an instance, the security group should not open 8000 to the world. The next lesson puts Nginx on port 80 and leaves 8000 for Nginx alone.
Workers are processes, not threads you guess
Three workers means three processes that can each handle a request. A worker blocked on the database does not block the others. More workers than CPU cores mostly wait. Start with two or three on a t3.micro and raise the number only when requests are waiting and the CPU is idle.
Gunicorn does not serve your CSS and JavaScript well. Django’s collectstatic gathers those files into STATIC_ROOT. Something else has to hand them to the browser. That something is Nginx.