Headers – The Source and Solution of All Problems

After working with Docker for a while and especially Docker Compose you start to take a lot of boring configuration for granted. I remember beginning web development and absolutely hating deploying projects to AWS instances because of the scary “reverse proxy” and setting up NGINX. Who knows how many times I googled “does flask need nginx” or “nodejs/express without nginx” when I just wanted to run a simple little script with a few endpoints and didn’t care about https.

With Docker all of these things become incredibly simple – build a React frontend and copy the dist directory to an NGINX image with a basic config file, expose port 80/443, run certbot, good to go. You can even do port forwarding to hit all your backend services (micro service architectures seem easy without some sort of message queue as this point too) and avoid any CORS/CSRF issues.

The Problem

Most of this blog is documenting silly issues I’ve run into to help me remember what I did to address it and this post is no different – I’ve been maintaining a small Django app at work that is accessible through a separate React frontend that is split into two flavors, a production version and a staging environment that we can test new features on real data. Unfortunately this environment has zero access to the internet and can only see our organization’s version control system to pull source code down. Because of this, I have not been using Docker.

I know – it should be trivial to build Docker containers and then install them on the machine as part of a CI/CD process but the machine doesn’t even have Docker installed. Obviously there are ways around this but since my core responsibilities don’t really include being a sysadmin for this machine I’ve been getting by with a couple systemd scripts to run an NGINX server and the responsible Django apps on the bare metal. Not ideal, but works well enough.

The biggest issue has been with Django’s admin interface. I got around the problem of sending post requests by decorating almost everything with the @csrf_exempt tag (whoops, but it’s internal only with no real auth) but since the Django admin is built in there aren’t any options for this.

I’ve been running the prod instance as prod but DEBUG=True for the staging environment, and the error message never really helped. It would say that “CSRF verification failed” and list the domain as not a trusted host. I’ve had each domain (and port) in the CSRF_TRUSTED_ORIGINS list forever but that didn’t seem to help.

The Solution

I like to keep a barebones NGINX config file for a couple reasons – I don’t like setting them up bare metal and if they are in docker they are usually set and forget. In this case I went a little too barebones, only focusing on setting the proxy_pass for my backend endpoints, the admin root, and static files coming from the Django server.

The critical missing piece was the ‘standard’ include proxy_params; line

This installation of NGINX didn’t come with a proxy_params file, so adding that instantly caused an error, and rather than recreate the whole thing I added the couple needed lines to make sure the right headers were making it to the server:

  • Host
  • X-Real-IP
  • X-Forwarded-For
  • X-Forwarded-Proto

As soon as this was added to the location blocks everything went back to normal.

I think this comes down to another reason why I’ve been liking Go and FastAPI for development – while more ‘boilerplate’ needed to be added the removal of some magic makes it very clear why certain things aren’t working correctly.

In a normal Django project that also manages serving the frontend this isn’t an issue, and had I been able to use my standard docker-compose project template this also would have been avoided – in this case I’m happy to have run into it to get a little better understanding of how Django handles requests and header checks to avoid it in the future. It’s also made using Django a bit less annoying to use for other projects so maybe I’ll take another look at using it when I can, since the ORM it ships with is hard to beat if it’s performant enough for a use case.

Leave a comment