Search code examples
reactjsdjangodjango-rest-frameworkdjango-react

404 Error: unable to serve static react build with django rest framework, unless I go to /index.html


I have a React front end that I turned into static files. I used npm run build to make the folder. Then I configured my Django Rest Framework:

settings.py

FRONTEND_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'frontend', 'build'))

urls.py

re_path(r'^(?P<path>.*)$', serve, { 'document_root': settings.FRONTEND_ROOT }),

when I got to the localhost:8000, I get a 404 page. If howerver, i got to localhost:8000/index.html, then I dont get a 404 but the react app. The css and html are not loaded though.

Is this a proper way to connect the static react to my django backend? Am I missing a step?


Solution

  • Django serve is rather for serving static files in development: docs.

    For development, I just run react dev server in one terminal and django dev server in the second terminal.

    For productions you have few options to serve static files:

    • take a look at WhiteNoise for serving static files in django (I had example somewhere how I config it, but cant find it right now, please let me know if you will need),
    • upload static react files to S3 and serve with CDN (in the case of AWS deployment and large traffic)
    • if you would like to keep everything in one machine, then I would recommend docker-compose. It is nice solution because you can serve react with nginx. What is more, you can use docker-compose to renew SSL certificate from Let's Encrypt. This is the option that I'm using. Below my docker-compose.

    I'm working on a complete tutorial on how to build your own SaaS application with Django and React from scratch. The docker-compose is from tutorial.

    # docker-compose
    version: '2'
    
    services:
        nginx: 
            restart: unless-stopped
            build:
                context: .
                dockerfile: ./docker/nginx/Dockerfile
            ports:
                - 80:80
                - 443:443
            volumes:
                - static_volume:/app/backend/server/django_static
                - ./docker/nginx:/etc/nginx/conf.d
                - ./docker/nginx/certbot/conf:/etc/letsencrypt
                - ./docker/nginx/certbot/www:/var/www/certbot
            depends_on: 
                - backend
        certbot:
            image: certbot/certbot
            restart: unless-stopped
            volumes:
                - ./docker/nginx/certbot/conf:/etc/letsencrypt
                - ./docker/nginx/certbot/www:/var/www/certbot
            entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"       
        backend:
            restart: unless-stopped
            build:
                context: .
                dockerfile: ./docker/backend/Dockerfile
            volumes:
                
            entrypoint: /app/docker/backend/wsgi-entrypoint.sh
            volumes:
                - .:/app
                - static_volume:/app/backend/server/django_static
            ports:
                - 8003:8003
            expose:
                - 8003        
    
    volumes:
        static_volume: {}
    

    The nginx dockerfile is in two steps:

    1. Build react
    2. Start nginx
    # nginx dockerfile
    # build environment
    FROM node:13.12.0-alpine as build
    
    ADD ./frontend /app/frontend/
    WORKDIR /app/frontend
    RUN npm install
    RUN npm run build
    
    # production environment
    FROM nginx:stable-alpine
    COPY --from=build /app/frontend/build /usr/share/nginx/html
    CMD ["nginx", "-g", "daemon off;"]
    

    default.conf file

    server {
        listen 80;
        server_name yourdomain.com;
        server_tokens off;
    
        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }
    
        location / {
            return 301 https://$host$request_uri;
        }
    }
    
    server {
        listen 443 ssl;
        server_name yourdomain.com;
        server_tokens off;
    
        ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    
        client_max_body_size 20M;
    
        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
            try_files $uri $uri/ /index.html;
        }
    
        location /api {
            try_files $uri @proxy_api;
        }
    
        location @proxy_api {
            proxy_set_header X-Forwarded-Proto https;
            proxy_set_header X-Url-Scheme $scheme;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_redirect off;
            proxy_pass   http://backend:8003;
        }
    
        location /django_static/ {
            autoindex on;
            alias /app/backend/server/django_static/;
        }
    }
    

    dockerfile for django

    FROM python:3.8.3-alpine
    
    WORKDIR /app
    ADD ./backend/requirements.txt /app/backend/
    
    
    RUN pip install --upgrade pip
    RUN pip install gunicorn
    RUN pip install -r backend/requirements.txt
    
    ADD ./backend /app/backend
    ADD ./docker /app/docker
    
    RUN rm -f /app/backend/server/db.sqlite3
    

    wsgi-entrypoint.sh

    #!/bin/sh
    
    #ls -al /app/backend/server/static/client/static/js
    
    until cd /app/backend/server
    do
        echo "Waiting for server volume..."
    done
    
    until ./manage.py migrate
    do
        echo "Waiting for db to be ready..."
        sleep 2
    done
    
    ls /app/backend/server
    
    ./manage.py collectstatic --noinput
    
    gunicorn server.wsgi --bind 0.0.0.0:8003 --workers 4 --threads 4
    #./manage.py runserver 0.0.0.0:8003  
    

    init-letsencrypt.sh script from https://medium.com/@pentacent/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71 I highly recommend this article!

    #!/bin/bash
    
    if ! [ -x "$(command -v docker-compose)" ]; then
      echo 'Error: docker-compose is not installed.' >&2
      exit 1
    fi
    
    domains=(yourdomain.com www.yourdomain.com)
    rsa_key_size=4096
    data_path="./docker/nginx/certbot"
    email="" # Adding a valid address is strongly recommended
    staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits
    
    if [ -d "$data_path" ]; then
      read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision
      if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
        exit
      fi
    fi
    
    
    if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then
      echo "### Downloading recommended TLS parameters ..."
      mkdir -p "$data_path/conf"
      curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf"
      curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem"
      echo
    fi
    
    echo "### Creating dummy certificate for $domains ..."
    path="/etc/letsencrypt/live/$domains"
    mkdir -p "$data_path/conf/live/$domains"
    docker-compose run --rm --entrypoint "\
      openssl req -x509 -nodes -newkey rsa:1024 -days 1\
        -keyout '$path/privkey.pem' \
        -out '$path/fullchain.pem' \
        -subj '/CN=localhost'" certbot
    echo
    
    
    echo "### Starting nginx ..."
    docker-compose up --force-recreate -d nginx
    echo
    
    echo "### Deleting dummy certificate for $domains ..."
    docker-compose run --rm --entrypoint "\
      rm -Rf /etc/letsencrypt/live/$domains && \
      rm -Rf /etc/letsencrypt/archive/$domains && \
      rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
    echo
    
    
    echo "### Requesting Let's Encrypt certificate for $domains ..."
    #Join $domains to -d args
    domain_args=""
    for domain in "${domains[@]}"; do
      domain_args="$domain_args -d $domain"
    done
    
    # Select appropriate email arg
    case "$email" in
      "") email_arg="--register-unsafely-without-email" ;;
      *) email_arg="--email $email" ;;
    esac
    
    # Enable staging mode if needed
    if [ $staging != "0" ]; then staging_arg="--staging"; fi
    
    docker-compose run --rm --entrypoint "\
      certbot certonly --webroot -w /var/www/certbot \
        $staging_arg \
        $email_arg \
        $domain_args \
        --rsa-key-size $rsa_key_size \
        --agree-tos \
        --force-renewal" certbot
    echo
    
    echo "### Reloading nginx ..."
    docker-compose exec nginx nginx -s reload
    

    I pasted here the files from the tutorial, so you would need to change the directory structure to yours and the domain (sorry!). I'm still working on the tutorial. If you have any questions, I'm happy to help.