Deploying a Wagtail site to Fly.io Part 4: Static & Media Files

This post is part of a series:

  1. The Plan
  2. Configuration
  3. Dockerising & Requirements
  4. Static & Media Files
  5. Deploying

Our Wagtail app needs to serve two different ‘categories’ of files:

We’ll use a couple of very useful Django packages to handle these.

🍞

bakerydemo comes preconfigured with both, so if you’re just here to deploy that, skip to the Media Files section below.

Static Files

As covered in Part 1 (but that was too many words ago), Django has no production-ready approach to serving static files.

Thankfully, WhiteNoise exists and offers a nice simple way to get the job done without any additional servers or services.

The Using WhiteNoise with Django docs already serve as a perfect starting point, so I won’t repeat them here. I recommend you only follow the setup up to step 3 for now - everything following may be useful in some situations but likely won’t be for this process.

You’ll need to add whitenoise==6.2.0 to your requirements.txt and update your settings to include something like:

# ...
MIDDLEWARE = [
    # ...
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # ...
]

# ...

STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

We’ve now told WhiteNoise to get involved whenever we ask for one of our ‘static’ files.

Fly does support a statics configuration which is an alternative approach to serving static files. However, WhiteNoise gives us a bit more flexibility allowing us to manage cache headers and serve compressed content.

Media Files

Media files are a little more complicated. By default, Django’s going to want to store any user uploaded files on the local filesystem.

With Fly.io (and many other similar services like Heroku), the filesystem given to your application is ephemeral - it will vanish whenever your application is shut down or re-deployed. That’s not great when you’re dealing with files your users are expecting to stick around.

Fly does have an option for persistent storage in Volumes, however these are tied to a region and server, so it limits how you can scale and distribute your application across regions, and means there’s a decent chance you could lose your data if that server goes away.

Unless you have a nice way to replicate your media across volumes, an external provider like we use here is recommended.

To solve this, we’re going to want to:

  1. Store those files somewhere a bit a more permanent.
  2. Tell Django to write and read files from that place.

The magic combination here is django-storages and Amazon S3.

Django Storages adds a set of custom ‘storage backends’ which give Django’s FileFields the ability to store and retrieve files from external locations. While it supports options like Google Cloud Storage and Azure Storage, we’ll be using Amazon S3 here.

Creating an S3 Bucket

First up, you’ll need an AWS account. If your account is new, you’ll get some 5GB of free storage for 12 months. If it’s not, 5GB is going to cost around $0.12 a month.

Once your account is set up and logged in, click your name in the top right and go to Security Credentials. Go to the Access keys section and click Create new access key. Take note of the generated Access Key ID and Secret Key in your password manager of choice.

A place to put files in S3 terminology is called a ‘bucket’. We’ll need to create a new one such that:

Yes, anyone will be able to read files from it. This is usually OK, but if you have a site where users might upload private documents or images that they expect are only accessible through your site (and not by visiting the link directly), this could be a problem.

Consider a package like wagtail-storages if you’d like a bit more control.

Setting this up can be a little daunting if you’re not familiar with AWS/S3, so we’ll take a little shortcut and use the handy s3-credentials tool which you can pip install in to your dev environment.

Once installed, we can get everything we need in one command:

s3-credentials create my-bakerydemo-media -c --public --access-key [your Access Key ID] --secret-key [your secret key]

(as S3 bucket names must be unique you’ll need to replace my-bakerydemo-media with a more unique name here).

This will go ahead and create:

and give you an output like:

Created bucket: my-bakerydemo-media
Attached bucket policy allowing public access
Created  user: 's3.read-write.my-bakerydemo-media' with permissions boundary: 'arn:aws:iam::aws:policy/AmazonS3FullAccess'
Attached policy s3.read-write.my-bakerydemo-media to user s3.read-write.my-bakerydemo-media
Created access key for user: s3.read-write.my-bakerydemo-media
{
    "UserName": "s3.read-write.my-test-bakerydemo-media",
    "AccessKeyId": "ABCDEFGHJIJKLMNOPQRS",
    "Status": "Active",
    "SecretAccessKey": "AbCdEFGHiJkLmNOPqrsTUVwxYZ12345678901234",
    "CreateDate": "2022-08-31 14:12:04+00:00"
}

You’ll want to note down your bucket name, AccessKeyID and SecretAccessKey.

🍞

If you’re working with bakerydemo, you’re done - it’s already configured to work with django-storages so we’ll revisit this when we come to deploying the app.

Next, you’ll need to add django-storages[s3] to your requirements and update your settings to use the django-storages S3 backend:

if "AWS_STORAGE_BUCKET_NAME" in os.environ:
    AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME")
    AWS_DEFAULT_ACL = "public-read"
    AWS_QUERYSTRING_AUTH = False

    INSTALLED_APPS.append("storages")
    DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"

We’re making sure to use environment variables here so that if you specify an AWS_STORAGE_BUCKET_NAME variable this config is going to apply.


That was a big one. Let’s move on…