Migrating a Django App from Heroku to Render

I recently migrated a Django application from Heroku to Render. They are similar enough platforms that it was a pretty straightforward and smooth process but there were a couple things that caused some confusion and minor setbacks along the way. Hopefully this post will make the process even easier for you (or myself) in the future.

Render has pretty good guides for setting up projects on their platform. To migrate my Django app, I followed a process that was a combination of their deploying Django and Migrating from Heroku guides. I'll describe my process below and call out a couple parts of the process that had I known more ahead of time, the process would have been even smoother.

Step 1: Set up the render.yaml file

The render.yaml file is Render's Infrastructure-as-code spec. My approach was to replicate my Heroku app service descriptions and settings in the render.yaml file and include it in the root of my repo. The example from the deploying Django offered a good starting point. I added a Redis service instance for caching and then all the environment variables from my Heroku configuration. For the envVars section of the web service in the render.yaml file, the DATABASE_URL and REDIS_URL values were set up to pull the connection parameters from the Postgres and Redis settings using fromDatabase and fromService respectively. The other variables were set up with sync: False which will prompt the values to be inputted when the blueprint is deployed for the first time.

Here's roughly what my render.yaml file looked like:

services:
  - type: web
    name: margins-api-django
    env: python
    region: ohio
    plan: starter
    branch: main
    buildCommand: "./build.sh"
    startCommand: "gunicorn margins.wsgi:application"
    healthCheckPath: /health
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: margins-prod
          property: connectionString
      - key: REDIS_URL
        fromService:
          type: redis
          name: margins-cache
          property: connectionString
      - key: LOGGING_LEVEL
        sync: false
      - key: SECRET_KEY
        sync: false
      - key: SENTRY_DSN
        sync: false
      - key: DEBUG
        value: off
      - key: PYTHON_VERSION
        value: "3.9.13"
  - type: redis
    name: margins-cache
    ipAllowList:
      - source: 0.0.0.0/0
        description: everywhere
    plan: free
    region: ohio
databases:
  - name: margins-prod
    plan: free
    region: ohio
    databaseName: margins
    user: margins

A note about the Python version: as of this writing, Render defaults to Python 3.7 but this can be overridden by including the PYTHON_VERSION environment variable in web service.

Step 2: Deploy the blueprint

On the Blueprints section of the Render dashboard, click the "New Blueprint Instance" button and select your Github repo. You will be prompted to enter values for any environment variables where you set sync: false as described above.

If everything was configured correctly, you should see your web service and Postgres and Redis services spin up successfully.

Step 3: Migrate postgres data

If everything goes well with the first two steps, you should have a working web application but with an empty database. Copying over the data from your Heroku database is probably the trickiest part of this process and potentially the most stressful if your application has a lot of users and traffic.

The copy data from Postgres section of the Migrating from Heroku article is a really good walkthrough and I generally followed those same steps but there is one key thing I did differently.

The last step where the pg_restore command is run likely works well for a completely empty database however, per the deploying Django guide as well as how my Heroku app was set up, database migrations are run during the build step of the web app deployment process meaning the database has tables set up already even though most of them are empty. This caused me to get a lot of errors and missing data when I ran the pg_restore command the first time. What I did instead which worked great was to drop the database and then create it again and then run the pg_restore command on the new, completely empty database. Specifically, I followed the below steps:

  1. Log into your newly-created Postgres instance. Render makes this really easy by generating a full psql command for you that's specific to your database. You can find it on your Postgres instance page from the Render dashboard under the section, "Connecting from outside the cluster".
  2. Drop the DB. First, make note of the database name. It was the last part of the psql command or you can find it next to the "Database" parameter (below the port number) on the Render dashboard. From psql, run DROP DATABASE <database_name>;
  3. Re-create the DB. Run CREATE DATABASE <database_name>;
  4. Quit the postgres terminal by running \q
  5. Run the pg_restore command. The version I used was slightly different from the one in the guide in that it included the database username as specified in the blueprint although I'm not 100% if that was necessary: pg_restore --verbose --no-acl --no-owner -d <EXTERNAL CONNECTION STRING> -U <YOUR USER> latest.dump

Step 4: Update DNS Config

The Render guide covers this part pretty well.

Once the DNS settings propagate, you should hopefully have an application that's running smoothly on Render! Render is still a pretty new service but I've been really happy with it so far.

If you performed a similar migration and have any questions or additional tips, get in touch!.