Migrating a Django Website from Dreamhost Shared Server with Passenger to VPS with Gunicorn

With Dreamhost dropping support for Passenger, it took far too much work to migrate my site to Gunicorn. Maybe this will help someone else who’s going through the same thing.

Since Dreamhost removed Passenger support from shared hosting, I needed to move my Django website to a VPS. Their notice seemed helpful:

After March 31st, we will no longer be able to offer Passenger on Shared Hosting plans. Later this year, we will also begin to remove Passenger from all hosting plans at DreamHost – as such, sites running applications currently relying on Passenger (eg. React, Node, Python, Ruby, etc.) will need to be set up on a VPS using a Proxy Server.

In order to keep your sites that require Passenger active in the short term, you will need to add (or request) one of the four currently-available VPS service tiers available on the VPS page of your account control panel. If you would like assistance determining which of these tiers best suits your needs, please don't hesitate to reply to this email.

After that, we strongly encourage you to look into setting up your Passenger sites using a Proxy Server, as well as setting up Guincorn and Linger to allow for persistent processes. Please note that if you are currently running Ruby, we are still extensively testing alternatives to Gunicorn and additional information regarding those alternatives will be added to our knowledge base in the future.

Moving to a VPS

The first step, moving the site to a VPS, was easiest one. I purchased the VPS plan, then I needed to create a new user on the VPS server. Migrating the site is a matter of changing the domain user to the VPS user.

Dreamhost’s documentation on “Migrating a single website to a different server.”

Creating a New Virtual Environment

Unfortunately the migration of site files breaks the old python virtual environment. I installed the same python version to the new server and created a new virtual environment for the site.

Configuring Gunicorn

Unfortunately, the same site on VPS didn’t just work. I don’t know what would have been involved to keep it “active in the short term,” as expressed in Dreamhost’s notice, because they’ve removed all Passenger-related information from their documentation.

I moved forward with their long-term suggestion, Gunicorn. I configured a proxy server in the panel and installed Gunicorn to my virtual environment.

According to the Dreamhost documentation, the command to start Gunicorn is

GUNICORN_CMD_ARGS="--bind=example.com:8002" gunicorn myapp:app --log-level=debug

where the domain is your domain, the port is the one configured for the proxy server.

For Django, the command needs the project to be on the Python path, saying “the simplest way to ensure that is to run this command from the same directory as your manage.py file.” For my site, configured to run on port 8000, with a Django project name of orangegnome, executed from the directory containing the manage.py file, with my virtual environment activated, my command looks like

GUNICORN_CMD_ARGS="--bind=orangegnome.com:8000" gunicorn orangegnome --log-level=debug

I navigated to orangegnome.com, and it loaded 🎉, but without any CSS 🤦‍♂️.

Configuring Linger

To keep the Gunicorn server to persist outside of my shell session, Dreamhost advises using Linger.

I followed the documentation for the orangegnome.service file with some key points:

  • The WorkingDirectory path is to the django folder with the manage.py file.
  • Instead of myapp:app in the ExecStart, I used my Django project name, orangegnome.wsgi

Starting and enabling according to the Dreamhost docs got the site working, but still without CSS.

Serving Static Files

Django makes a big deal about not serving static files in production. I don’t know how Passenger was making it happen, but with the reverse proxy + Gunicorn set up, all site requests are routed to Gunicorn serving the Django app, so all of the static assets were returning 404s.

To get around this, I set up a subdomain on the VPS, assets.orangegnome.com with the same user. Then I configured these settings in settings.py

STATIC_URL = 'https://assets.orangegnome.com/orangegnome.com/static/'
MEDIA_URL = 'https://assets.orangegnome.com/orangegnome.com/media/'
STATIC_ROOT = os.path.join(BASE_DIR, '../../assets.orangegnome.com/orangegnome.com/static')
MEDIA_ROOT = os.path.join(BASE_DIR, '../../assets.orangegnome.com/orangegnome.com/media')

Running manage.py collectstatic successfully copied the static files to the other domain’s folder. I manually copied the media files over.

I restarted the orangegnome service, reloaded my site, and the CSS loaded 🎉, but checking my network pane in dev tools revealed that fonts were getting blocked with CORS errors.

Adding CORS Headers to the Subdomain

I added an .htaccess file to the `assets.orangegnome.com/orangegnome.com` directory with these values in accordance with the Dreamhost docs:

Header add Access-Control-Allow-Origin: "https://orangegnome.com"
Header add Access-Control-Allow-Methods: "GET"
Header add Access-Control-Allow-Headers: "Upgrade-Insecure-Requests"

I reloaded my site and now the fonts are loading 🎉

Logging into /admin

With my site up and running I went to post a note about it. Attempting to log in to the admin area greeted me with “CSRF verification failed. Request aborted.”

This StackOverflow answer did the trick. I added this to my settings.py

CSRF_TRUSTED_ORIGINS = ["https://orangegnome.com"]

My guess is that since the site is now running locally, but exposed through the reverse proxy, I now need to provide the external URL for the CSRF settings.

A Couple of Final Questions

I still have a couple of unresolved items. Another possible way to solve the static assets problem would be to configure the server to serve the /static and /media requests directly and route all other requests to Gunicorn. I don’t know how to do this, or if it’s possible with Dreamhost’s reverse proxy feature. I have a support ticket in to try and understand more.

Additionally, while working on this post, my “if I’m logged in, let me view unpublished posts” functionality appears to be broken. I get a 404 when trying to do that. I’ll need to dig in a bit more on that, but I have to wonder if it’s something to do with the reverse proxy configuration.