buy the book ribbon

Deploying Our New Code

It’s time to deploy our brilliant new validation code to our live servers. This will be a chance to see our automated deploy scripts in action for the second time. Let’s take the opportunity to make a little deployment checklist.

At this point I always want to say a huge thanks to Andrew Godwin and the whole Django team. In the first edition, I used to have a whole long section, entirely devoted to migrations. Since Django 1.7, migrations now "just work", so I was able to drop it altogether. I mean yes this all happened nearly ten years ago, but still—​open source software is a gift. We get such amazing things, entirely for free. It’s worth taking a moment to be grateful, now and again.

The Deployment Checklist

Let’s make a little checklist of pre-deployment tasks:

  1. We run all our unit tests and functional tests (FTs) in the regular way—just in case!

  2. We rebuild our Docker image and run our tests against Docker on our local machine.

  3. We deploy to staging, and run our FTs against staging.

  4. Now we can deploy to prod.

A deployment checklist like this should be a temporary measure. Once you’ve worked through it manually a few times, you should be looking to take the next step in automation: continuous deployment straight to production using a CI/CD (continuous integration/continuous development) pipeline. We’ll touch on this in [chapter_25_CI].

A Full Test Run Locally

Of course, under the watchful eye of the Testing Goat, we’re running the tests all the time! But, just in case:

$ cd src && python manage.py test
[...]

Ran 37 tests in 15.222s

OK

Quick Test Run Against Docker

The next step towards production is running things in Docker. This was one of the main reasons we went to the trouble of containerising our app: to reproduce the production environment as faithfully as possible on our own machine.

So let’s rebuild our Docker image and spin up a local Docker container:

$ *docker build -t superlists . && docker run \
    -p 8888:8888 \
    --mount type=bind,source="$PWD/src/db.sqlite3",target=/src/db.sqlite3 \
    -e DJANGO_SECRET_KEY=sekrit \
    -e DJANGO_ALLOWED_HOST=localhost \
    -e DJANGO_DB_PATH=/home/nonroot/db.sqlite3 \
    -it superlists
 => [internal] load build definition from Dockerfile                  0.0s
 => => transferring dockerfile: 371B                                  0.0s
 => [internal] load metadata for docker.io/library/python:3.14-slim   1.4s
 [...]
 => => naming to docker.io/library/superlists                         0.0s
+ docker run -p 8888:8888 --mount
type=bind,source="$PWD/src/db.sqlite3",target=/src/db.sqlite3 -e
DJANGO_SECRET_KEY=sekrit -e DJANGO_ALLOWED_HOST=localhost -e EMAIL_PASSWORD -it
superlists
[2025-01-27 21:29:37 +0000] [7] [INFO] Starting gunicorn 22.0.0
[2025-01-27 21:29:37 +0000] [7] [INFO] Listening at: http://0.0.0.0:8888 (7)
[2025-01-27 21:29:37 +0000] [7] [INFO] Using worker: sync
[2025-01-27 21:29:37 +0000] [8] [INFO] Booting worker with pid: 8

And now, in a separate terminal, we can run our FT suite against the Docker:

$ TEST_SERVER=localhost:8888 python src/manage.py test functional_tests
[...]
......
 ---------------------------------------------------------------------
Ran 6 tests in 17.047s

OK

Looking good! Let’s move on to staging.

Staging Deploy and Test Run

Here’s our ansible-playbook command to deploy to staging:

$ ansible-playbook --user=elspeth -i staging.ottg.co.uk, infra/deploy-playbook.yaml -vv
[...]

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
[...]
ok: [staging.ottg.co.uk]

TASK [Install docker] **********************************************************
ok: [staging.ottg.co.uk] => {"cache_update_time": [...]

TASK [Add our user to the docker group, so we don't need sudo/become] **********
ok: [staging.ottg.co.uk] => {"append": true, "changed": false, [...]

TASK [Reset ssh connection to allow the user/group change to take effect] ******

TASK [Build container image locally] *******************************************
changed: [staging.ottg.co.uk -> 127.0.0.1] => {"actions": ["Built image
[...]

TASK [Export container image locally] ******************************************
changed: [staging.ottg.co.uk -> 127.0.0.1] => {"actions": ["Archived image [...]

TASK [Upload image to server] **************************************************
changed: [staging.ottg.co.uk] => {"changed": true, "checksum": [...]

TASK [Import container image on server] ****************************************
changed: [staging.ottg.co.uk] => {"actions": ["Loaded image superlists:latest
[...]

TASK [Ensure .secret-key file exists] ******************************************
ok: [staging.ottg.co.uk] => {"changed": false, "dest":
[...]

TASK [Read secret key back from file] ******************************************
ok: [staging.ottg.co.uk] => {"changed": false, "content":
[...]

TASK [Ensure db.sqlite3 file exists outside container] *************************
changed: [staging.ottg.co.uk] => {"changed": true, "dest": [...]

TASK [Run container] ***********************************************************
changed: [staging.ottg.co.uk] => {"changed": true, "container":
[...]

TASK [Run migration inside container] ******************************************
changed: [staging.ottg.co.uk] => {"changed": true, "rc": 0, "stderr": "",
[...]

PLAY RECAP *********************************************************************
staging.ottg.co.uk         : ok=12   changed=7    unreachable=0    failed=0
skipped=0    rescued=0    ignored=0
If your server is offline because you ran out of free credits with your provider, you’ll have to create a new one. Skip back to [chapter_11_server_prep] if you need.

And now we run the FTs against staging:

$ TEST_SERVER=staging.ottg.co.uk python src/manage.py test functional_tests
OK

Hooray!

Production Deploy

As all is looking well, we can deploy to prod!

$ ansible-playbook --user=elspeth -i www.ottg.co.uk, infra/deploy-playbook.yaml -vv

What to Do If You See a Database Error

Because our migrations introduce a new integrity constraint, you may find that it fails to apply because some existing data violates that constraint. For example, here’s what you might see if any of the lists on the server already contain duplicate items:

sqlite3.IntegrityError: columns list_id, text are not unique

At this point, you have two choices:

  1. Delete the database on the server and try again—after all, it’s only a toy project!

  2. Create a data migration. You can find out more in the Django migrations docs.

How to Delete the Database on the Staging Server

Here’s how you might do option 1:

ssh [email protected] rm db.sqlite3

The ssh command takes an arbitrary shell command to run as its last argument, so we pass in rm db.sqlite3. We don’t need a full path because we keep the SQLite database in our home folder.

Try not to accidentally delete your production database.

Wrap-Up: git tag the New Release

The last thing to do is to tag the release in our version control system (VCS)—it’s important that we’re always able to keep track of what’s live:

$ git tag -f LIVE  # needs the -f because we are replacing the old tag
$ export TAG=`date +DEPLOYED-%F/%H%M`
$ git tag $TAG
$ git push -f origin LIVE $TAG
Some people don’t like to use push -f and update an existing tag, and will instead use some kind of version number to tag their releases. Use whatever works for you.

And on that note, we can wrap up the last of the concepts we discussed in [part3], and move on to the more exciting topics that comprise [part4]. Can’t wait!

Deployment Procedure Review

We’ve done a couple of deploys now, so this is a good time for a little recap:

  • Deploy to staging first.

  • Run our FTs against staging.

  • Deploy to live.

  • Tag the release.

Deployment procedures evolve and get more complex as projects grow, and it’s an area that can become hard to maintain—full of manual checks and procedures—if you’re not careful to keep things automated. There’s lots more to learn about this, but it’s out of scope for this book. Dave Farley’s video on continuous delivery is a good place to start.

Comments