python

Django

Media and static files through django-storages
verified in CIv1.14+Django documentation ↗

Install

shell
django>=4.2
django-storages[s3]>=1.14

Configure

Configure the endpoint, region, credential and bucket in this tool’s vocabulary. Path-style addressing is recommended for dotted bucket names and clients without nested wildcard TLS, but virtual-hosted addressing is also supported.

In DjangoSet it to
OPTIONS.endpoint_urlhttps://s3.canada.popcloud.ca
OPTIONS.region_namecanada
OPTIONS.access_keyPCAK00EXAMPLEKEYID00
OPTIONS.secret_key<your secret access key>
OPTIONS.addressing_stylepath
Environment
example values
export AWS_ACCESS_KEY_ID=PCAK00EXAMPLEKEYID00
export AWS_SECRET_ACCESS_KEY=<your secret access key>
export AWS_ENDPOINT_URL=https://s3.canada.popcloud.ca
export AWS_REGION=canada
export POPCLOUD_BUCKET=pc-your-org-media

These are example values. Sign in and every snippet on this site fills in with your own endpoint, key and bucket.

Python
# settings.py — Django 4.2 and later. There is nothing PopCloud-specific in the
# backend: it is the same S3 backend, pointed at a different endpoint.
STORAGES = {
    "default": {
        "BACKEND": "storages.backends.s3.S3Storage",
        "OPTIONS": {
            "bucket_name": os.environ["POPCLOUD_BUCKET"],
            "endpoint_url": os.environ["AWS_ENDPOINT_URL"],  # https://s3.<region>.popcloud.ca
            "region_name": os.environ["AWS_REGION"],         # canada
            "access_key": os.environ["AWS_ACCESS_KEY_ID"],
            "secret_key": os.environ["AWS_SECRET_ACCESS_KEY"],
            # Recommended for predictable custom-endpoint behavior. PopCloud
            # also supports virtual-hosted requests for single-label buckets.
            "addressing_style": "path",
            # Keep signed media URLs on modern AWS Signature Version 4 too.
            "signature_version": "s3v4",
            # PopCloud has no bucket ACLs — access is a property of the
            # credential and of the bucket, not of each object.
            "default_acl": None,
            # Keep uploads from silently overwriting each other; Django appends
            # a suffix instead.
            "file_overwrite": False,
            # Media URLs come back signed and expiring. Set querystring_auth to
            # False only for a bucket you have made public.
            "querystring_auth": True,
            "querystring_expire": 3600,
        },
    },
    "staticfiles": {
        "BACKEND": "storages.backends.s3.S3Storage",
        "OPTIONS": {
            "bucket_name": os.environ["POPCLOUD_BUCKET"],
            "endpoint_url": os.environ["AWS_ENDPOINT_URL"],
            "region_name": os.environ["AWS_REGION"],
            "access_key": os.environ["AWS_ACCESS_KEY_ID"],
            "secret_key": os.environ["AWS_SECRET_ACCESS_KEY"],
            "addressing_style": "path",
            "default_acl": None,
            "location": "static",
            # Static files are public and immutable — serve them unsigned from a
            # public bucket, with a long cache header.
            "querystring_auth": False,
            "object_parameters": {"CacheControl": "max-age=31536000, immutable"},
        },
    },
}

Use it

Save a file

Python
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage

# In a model this is just a FileField or ImageField — the storage backend is
# what changed, not your code.
stored_name = default_storage.save(name, ContentFile(content))

Read a file

Python
from django.core.files.storage import default_storage

with default_storage.open(name) as handle:
    data = handle.read()

Build a URL

Python
from django.core.files.storage import default_storage

# With querystring_auth on, this is a presigned URL that expires — safe to
# hand to a browser for a private object.
url = default_storage.url(name)

Delete an object

Python
from django.core.files.storage import default_storage

default_storage.delete(name)

What to watch for

These apply to Django specifically. The full list covers the platform.

You’ll see
405 MethodNotAllowed from CreateBucket or DeleteBucket — `aws s3 mb`, `mc mb`, or a tool provisioning its own bucket on first run.
Why
Bucket lifecycle belongs to the control plane, which also allocates the storage account, the data-plane key and the CORS rules that come with it. Letting the edge create buckets would create half of one.
Instead
Create buckets in the dashboard, or with `POST /v1/buckets` on the control-plane API. Everything object-level then works normally against that bucket.
You’ll see
405 MethodNotAllowed on PutBucketPolicy, PutBucketLifecycleConfiguration, PutBucketWebsite, PutBucketReplication, PutBucketNotificationConfiguration, PutBucketTagging, PutPublicAccessBlock and their delete counterparts; the matching Get calls return 501 or are refused upstream.
Why
These configure infrastructure behaviour that PopCloud manages itself. Access control is expressed through credential scopes and the bucket's public flag rather than through bucket policies.
Instead
Use credential scopes for access control (permissions plus a bucket allowlist, optionally with an expiry), the bucket's public flag for anonymous reads, and the control plane for CORS. Tools that reconcile bucket configuration — Terraform's `aws_s3_bucket` in particular — must be limited to object-level resources.

How this page is kept true

Every snippet above was extracted from examples/python/django-storages, a program that uploads, downloads, compares bytes, lists, presigns and cleans up after itself. It runs in CI against a live sandbox organisation via make docs-verify-python. If it stops passing, this page is wrong and we treat that as a bug in the product.

Not yet run against the live sandbox — the runner is wired, the first verified run stamps this line.