Deploying a Wagtail site to Fly.io Part 4: Static & Media Files
Part of the "Wagtail on Fly.io" series
Posts in this series:
Our Wagtail app needs to serve two different ‘categories’ of files:
- Static files - things that are in our codebase like CSS and JS and don’t change unless we deploy a new version of the site.
- Media files - images and documents uploaded by our site users and content editors.
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:
- Store those files somewhere a bit a more permanent.
- 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 FileField
s 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:
- Our site is allowed to put files in to it
- Anyone on the internet can read files from it
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:
- A bucket which is publicly accessible
- An AWS user which has write access to the bucket
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.