Django Ubuntu 배포 (full version) (nginx + gunicorn + socket)

IOVEIT·2023년 9월 30일
0

(1) settings.py에 DEBUG=FALSE로 수정할 것
(2) settings.py에 database 호스트 수정할 것
(3) html 파일에 호스트 수정할 것
(4) settings.py에 주석처리 해제할 것: CSRF_COOKIE_DOMAIN = ".yourdomain.kr"

1. Bring your code to a server.

〈 from files 〉
C:\> scp -P server_port uploadfile.7z id@your.host.com:/home/yourid

〈 from git 〉
$ git config --global user.name "user_name"
$ git config --global user.email "user_id"
$ git clone http://000.000.000.000:8080/user_id/project_name.git

2. Uncompress the file.

〈 from files 〉
$ cd /home/yourid
$ mkdir newapp
$ mv uploadfile.7z newapp/
$ cd newapp
$ 7zr x uploadfile.7z

3. Check the python version.

$ python3
$ python3.10
$ python3.11 😀 This one,
$ apt install python3.11-dev

4. Set a virtual environment.

$ in your project
$ python3.11 venv venv 😀 the dicrectory you made above
$ cd venv/bin
$ source ./activate

😀 you will see bash like below,
(newapp) yourid@hostname:~/python/newapp/bin$
Finally, the environment has been set for your python version.

$ python
Python 3.11.5 (main, Aug 25 2023, 13:19:50) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.

5. Install libraries

$ python -m pip install django
$ python -m pip install mysqlclient
$ python -m pip install django-environ
$ python -m pip install django-bootstrap5
$ python -m pip install django-debug-toolbar
$ python -m pip install tqdm
$ pip install uvicorn[standard]

6. Set database

$ mariadb -uroot -p
MariaDB [(none)]> create user 'admin'@'%' identified by 'pw';
MariaDB [(none)]> create database db_name;
MariaDB [(none)]> grant all privileges on db_name.* to admin@'%' identified by 'pw';

7. Migrate web

$ python manage.py makemigrations appA
$ python manage.py migrate appA

$ python manage.py makemigrations appB
$ python manage.py migrate appB

$ python manage.py migrate

$ python manage.py createsuperuser
사용자 이름: id
이메일 주소:
Password:
Password (again)

$ python manage.py collectstatic

8. Install gunicorn

$ pip install gunicorn

$ ls -al

drwxrwxr-x 2 account account  4096 Sep 30 21:17 bin
drwx------ 3 account account  4096 Sep 28 15:41 your_app >> wgsi.py >> applicaion 😀
drwxrwxr-x 4 account account  4096 Sep 30 17:47 include
drwxrwxr-x 3 account account  4096 Sep 30 17:31 lib
-rw-rw-r-- 1 account account   625 Sep 27 22:20 manage.py
drwx------ 4 account account  4096 Sep 26 22:29 webapp

$ ls -al ./bin
-rwxrwxr-x 1 account account 252 Sep 30 21:17 gunicorn

$ ./bin/gunicorn --bind 0:8000 your_app.wsgi:application

  • your_app. >> wsgi.py 파일이 들어있는 디렉토리 명칭
  • wsgi: >> wsgi.py 파일의 파일명 (마침표가 아니라, 콜론(:)임에 주의)
  • application >> wsgi.py 파일 내, get_wsgi_application()을 받는 변수명
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "😀your_app.settings")
application = get_wsgi_application()
[2023-09-30 21:23:36 +0900] [151098] [INFO] Starting gunicorn 21.2.0
[2023-09-30 21:23:36 +0900] [151098] [INFO] Listening at: http://0.0.0.0:8000 (151098)
[2023-09-30 21:23:36 +0900] [151098] [INFO] Using worker: sync
[2023-09-30 21:23:36 +0900] [151100] [INFO] Booting worker with pid: 151100
[2023-09-30 21:23:41 +0900] [151098] [INFO] Handling signal: int
[2023-09-30 21:23:41 +0900] [151100] [INFO] Worker exiting (pid: 151100)
[2023-09-30 21:23:41 +0900] [151098] [INFO] Shutting down: Master

9. Register your web in service.

$ sudo vi /etc/systemd/system/your_app.service

[Unit]
Description=your web server
After=network.target

[Service]
User=your_account
Group=www-data
WorkingDirectory=/home/your_account/python/your_project😀
ExecStart=       /home/your_account/python/your_project/venv/bin/gunicorn😀 \
          dadam.wsgi:application😀 \
          --workers 4 \
          --log-config log.ini😀 \
          --worker-class uvicorn.workers.UvicornWorker \
          --bind unix:/tmp/dadam_watch.sock

[Install]
WantedBy=multi-user.target

$ sudo systemctl start your_app.service
$ sudo systemctl status your_app.service

your_app.service - gunicorn daemon
Active: failed (Result: exit-code) since Sat 2023-09-30 22:46:22 KST; 4s ago
 .
 .
 . 
Sep 30 22:46:22 hostname gunicorn[153399]: ModuleNotFoundError: No module named 'uvicorn'
Sep 30 22:46:22 hostname systemd[1]: your.service: Main process exited, code=exited, status=1/FAILURE
Sep 30 22:46:22 toylabs systemd[1]: your.service: Failed with result 'exit-code'.

$ pip install uvicorn[standard]
$ sudo systemctl start your_app.service
$ sudo systemctl status your_app.service

your_app.service - gunicorn daemon
 .
 .
 Active: active (running) 

$ sudo vi /~/nginx/sites-available/default
location /

$ sudo systemctl enable your_app.service
$ sudo systemctl restart your_app.service
$ sudo systemctl status your_app.service

TypeError: WSGIHandler.__call__() missing 1 required positional argument: 'start_response'

`

10. set whitenoise

$ python -m pip install whitenoise

MIDDLEWARE = [
		.
	"whitenoise.middleware.WhiteNoiseMiddleware",
		.
		.
		.

`

11. 킹 받은 에러, 해결

기본 코드는 dependency injector 공식웹에서 제공하는 example인데, 서비스로 등록하여 실행하면 다음과 같은 오류가 로그로 나온다.

  1. TypeError: WSGIHandler.call() missing 1 required positional argument: 'start_response'
  2. django.core.exceptions.DisallowedHost: Invalid HTTP_HOST header: The domain name provided is not valid according to RFC 1034/1035.

✔ 콘솔에서 명령어를 실행하면서 나오는 에러를 해결하자!

11.1. TypeError: WSGIHandler.call() missing

$ gunicorn [your_app].wsgi:application --workers 4 --log-config log.ini --worker-class uvicorn.workers.UvicornWorker --bind unix:/tmp/you_named_it.sock

😀 서비스로 실행하지 않고 콘솔에서 직접 실행하면 잘 돌아가는 것처럼 보이지만, 웹브라우저로 접속하면 다음과 같은 에러가 발생한다. "TypeError: WSGIHanler.call() missing", "start_response"

Sep 30 23:23:15 host gunicorn[154634]:   File "....lib/python3.1>
Sep 30 23:23:15 host gunicorn[154634]:     result = await app(  # type: ignore[func-returns-value]
Sep 30 23:23:15 host gunicorn[154634]:              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Sep 30 23:23:15 host gunicorn[154634]:   File ".../lib/python3.1>
Sep 30 23:23:15 host gunicorn[154634]:     return await self.app(scope, receive, send)
Sep 30 23:23:15 host gunicorn[154634]:            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Sep 30 23:23:15 host gunicorn[154634]:   File ".../lib/python3.1>
Sep 30 23:23:15 toylabs gunicorn[154634]:     instance = self.app(scope)
Sep 30 23:23:15 toylabs gunicorn[154634]:                ^^^^^^^^^^^^^^^

TypeError: WSGIHandler.__call__() missing 1 required positional argument: 'start_response'

wsgi -> asgi로 변경하니, 일단 작동한다. (asgi를 사용하는 서버가 uvicorn이고, uvicorn worker를 사용하므로, wsgi가 아닌 asgi를 넣는게 맞는지?)

$ gunicorn [your_app].asgi:application --workers 4 --log-config log.ini --worker-class uvicorn.workers.UvicornWorker --bind unix:/tmp/you_named_it.sock

공식 웹에 Python Web Server Gateway Interface(https://peps.python.org/pep-0333, Specification Details)를 살펴보면 application 객체는 반드시 2개 매개변수를 받아야 한다. 구쳬적인 예를 들어 "environ"과 "start_reponse"다. 반드시 이 명칭을 사용해야 하는 것은 아니다. (명칭은 변경이 가능하며) 서버나 게이트웨이는 반드시 2개 매개변수를 사용해서 application을 동작(invoke) 시켜야 한다. 따라서 wsgi를 이용하여 장고를 실행하려면 application = get_wsgi_application() 살펴봐야 할듯하나, 어차피 ASGI를 사용하는 것으로 결론!

The application object must accept two positional arguments. 
For the sake of illustration, we have named them environ and 
start_response, but they are not required to have these names. 
A server or gateway must invoke the application object using 
positional (not keyword) arguments. 
(E.g. by calling result = application(environ, start_response) as shown above.)

11.2. DisallowedHost: Invalid HTTP_HOST header

😀 여전히 실행에는 문제가 없으나, 브라우저로 접속하면 브라우저에는 "BAD REQUEST"가 뜨고, 에러 메시지는 다음과 같이 발생한다.

[2023-09-30 23:49:47,727.727] ERROR [140297684964928] - Invalid HTTP_HOST header: 'your_domain.kr'. The domain name provided is not valid according to RFC 1034/1035.
 .
 .
 . 
  File ".../lib/python3.11/site-packages/django/middleware/common.py", line 48, in process_request
    host = request.get_host()
           ^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/django/http/request.py", line 150, in get_host
    raise DisallowedHost(msg)
django.core.exceptions.DisallowedHost: Invalid HTTP_HOST header: 'your.domain.kr'. The domain name provided is not valid according to RFC 1034/1035.

$ vi settings.py

DEBUG = False
ALLOWED_HOSTS = ["your.domain.kr", "localhost", "127.0.0.1", "server.ip"]😀

$ sudo vi /etc/nginx/sites-available/default

server {
        listen                  443 ssl;
        server_name             your.domain.kr;
        ssl_certificate         /etc/~.pem;
        ssl_certificate_key     /etc/~.pem;
        ssl_protocols           TLSv1 TLSv1.1 TLSv1.2;

        location / {
            include             proxy_params;
            proxy_pass          http://unix:/tmp/your_sock.sock;
            proxy_redirect      off;
            # 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_headers_hash_max_size         1024;
            proxy_headers_hash_bucket_size      512;
        }

`
주석처리한 이유는 proxy_params 파일에도 proxy_set_HEADER Host $host가 이미 있어서 중복으로 처리되기 때문이다. 아니 근데, fastapi를 gunicorn으로 실행할 때는 nginx에 아무런 문제가 발행하지 않다가, django에서 발생하는지 의문이다.

$ sudo cat /etc/nginx/proxy_params

proxy_set_header Host $http_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;

$ systemctl restart nginx

$ gunicorn [your_app].wsgi:application --workers 4 --log-config log.ini --worker-class uvicorn.workers.UvicornWorker --bind unix:/tmp/you_named_it.sock

$ sudo vi /etc/systemd/system/your_app.service

[Unit]
Description=gunicorn daemon
After=network.target

[Service]
User=your_account
Group=www-data
WorkingDirectory=/home/your_account/python/your_directory
ExecStart=       /home/your_account/python/your_directory/bin/gunicorn \
          dadam.😀asgi:application \
          --workers 4 \
          --log-config log.ini \
          --worker-class uvicorn.workers.UvicornWorker \
          --bind unix:/tmp/dadam_watch.sock

[Install]
WantedBy=multi-user.target

$ sudo systemctl daemon-reload
$ sudo systemctl start your.service
$ sudo systemctl status your.service

12. ajax로 POST 요청 시, Forbidden Error

Ajax로 POST 요청하면 Forbidden Error가 발생한다. 보통 설정을 꺼버리고 실행하는 분들도 있지만, 보안 기능인만큼 켜두고 실행하고 싶었다. 구글에서 검색하여 나온 결과들을 적용하면서 해결하였기 때문에, 무엇이 주요한 것인지가 아직은 불분명하다.

your.html

<script>
    $.ajaxSetup({
        headers: { "X-CSRFToken": '{{csrf_token}}' }
    });
</script>

settings.py

CSRF_TRUSTED_ORIGINS = ["https://sub.domain.kr"]
CSRF_COOKIE_DOMAIN = ".domain.kr"
CSRF_COOKIE_SECURE = True
SESSION_CSRF_COOKIE_SECURE = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
profile
EnCoCookLand

0개의 댓글