Django deployment with App Platform & S3 Space
Tags: django, webdev, tooling, tutorial
Published: 2022-09-14 18:27:48

## Preface

Recently i had to decide which hosting solution i want to use for my private blog. The blog was developed with the help of [Dev-Case](https://rburkhardt.com/projects/devcase/) and is based on Django.

Since i have read a lot of good things about **PaaS** in connection with Django lately, the decision was made quickly.

The providers to choose from were:

- DigitalOcean
- Railway
- Render

In the end, i decided to go with DigitalOcean because i have already had good experiences here (VPS, Space). Another plus for DO was that i didn't need another provider for the S3-Storage.

## Preparation

To be able to host my Django project with the App-Platform, i have made the following preparations:

### Environment variables

For this i use [django-environ](https://github.com/joke2k/django-environ).
Here are a few basic settings:

``` python
import os
from pathlib import Path
from django.core.management.utils import get_random_secret_key
import environ

BASE_DIR = Path(__file__).resolve().parent.parent

env = environ.Env()
env_file = os.path.join(BASE_DIR, ".env")
if os.path.isfile(env_file):
    env.read_env(env_file)

SECRET_KEY = env.str("SECRET_KEY", default=get_random_secret_key())

DEBUG = env.bool("DEBUG", default=False)

ALLOWED_HOSTS = env.list(
    "ALLOWED_HOSTS",
    default=[
        "127.0.0.1",
    ],
)
```

### Database

Here i decided to use Postgres.

Thanks to **django-environ** i can use a `DATABASE_URL` here.
A possibility to use SQLite should still exist but is a matter of personal taste.

``` python
DATABASE_URL = env.str("DATABASE_URL", default=False)

if DATABASE_URL:
    DATABASES = {"default": env.db()}
else:
    DATABASES = {
        "default": {
            "ENGINE": "django.db.backends.sqlite3",
            "NAME": BASE_DIR / "db.sqlite3",
        }
    }

```

### S3 Storage

To be able to use the S3 compatible storage later on (DO Space):

``` python
USE_S3_STORAGE = env.bool("USE_S3_STORAGE", default=False)
AWS_ACCESS_KEY_ID = env.str("AWS_ACCESS_KEY_ID", default="")
AWS_SECRET_ACCESS_KEY = env.str("AWS_SECRET_ACCESS_KEY", default="")
AWS_STORAGE_BUCKET_NAME = env.str("AWS_STORAGE_BUCKET_NAME", default="")
AWS_S3_REGION_NAME = env.str("AWS_S3_REGION_NAME", default="")
AWS_S3_ENDPOINT_URL = env.str("AWS_S3_ENDPOINT_URL", default="")
AWS_S3_CUSTOM_DOMAIN = env.str("AWS_S3_CUSTOM_DOMAIN", default="")
AWS_LOCATION = env.str("AWS_LOCATION", default="")
AWS_IS_GZIPPED = env.bool("AWS_IS_GZIPPED", default=False)
AWS_S3_FILE_OVERWRITE = env.bool("AWS_S3_FILE_OVERWRITE", default=True)
AWS_DEFAULT_ACL = env.str("AWS_DEFAULT_ACL", default="public-read")

STATICFILES_DIRS = (str(BASE_DIR.joinpath("static")),)
STATIC_ROOT = str(BASE_DIR.joinpath("staticfiles"))
MEDIA_ROOT = str(BASE_DIR.joinpath("media"))

if USE_S3_STORAGE:
    DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
    STATICFILES_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"

    STATIC_URL = f"https://{AWS_S3_ENDPOINT_URL}/{STATIC_ROOT}/"
    MEDIA_URL = f"https://{AWS_S3_ENDPOINT_URL}/{MEDIA_ROOT}/"

else:
    STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
    STATIC_URL = "static/"
    MEDIA_URL = "/media/"

```

### SSL

A valid SSL certificate is supplied automatically:

``` python
SECURE_SSL_REDIRECT = env.bool("SECURE_SSL_REDIRECT", default=False)
SECURE_HSTS_SECONDS = env.int("SECURE_HSTS_SECONDS", default=0)
SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool(
    "SECURE_HSTS_INCLUDE_SUBDOMAINS", default=False
)
SECURE_HSTS_PRELOAD = env.bool("SECURE_HSTS_PRELOAD", default=False)

SESSION_COOKIE_SECURE = env.bool("SESSION_COOKIE_SECURE", default=False)
CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE", default=False)
```

## PIP requirements

The following packages are required:

``` text
django
gunicorn
django-environ
psycopg2-binary
boto3
django-storages
```

With the help of the *requirements.txt* file, the buildtool later also recognizes that this is a python project.

## App Platform

Now that everything is ready, you can start creating the Django project on DO. A detailed instruction i save me at this point, because the docs and further post about it on the internet are very good.

Important settings are (example Dev-Case):

- run_command: `gunicorn dev_case.wsgi:application --bind 0.0.0.0:8080 --worker-tmp-dir /dev/shm`
- http_port: `8080`

Depending on the project structure:

``` yaml
routes:
  - path: /
source_dir: /
```

With a "dev" database a dynamic variable can be used as environment variable. Example: `${db.DATABASE_URL}`. In this case the name of the database would be `db`.

When using a managed-databases from DO, you should create the database manually and not via the GUI. Otherwise, errors may occur with a redeploy. I will link a post about it here soon.

_edited (2022-09-26):_ [link](https://rburkhardt.com/blog/django-deployment-postgres-dbaas/) to the post mentioned above.

 **After the build process completes:**

 Access your app’s console through the console tab and run the following commands:

- `python3 manage.py migrate` for the initial database migrations
- `python3 manage.py createsuperuser` to create an administrative user

## S3 and CORS

To prevent later errors with CORS, make the following adjustment in your Space setting:

Your Space -> Settings -> CORS Configurations (Add):

- Add your domain (with wildcard) in "Origin", examples:
  - `*ondigitalocean.app`
  - `*example.com`
- Allow/Check:
  - GET, HEAD

This should solve the problems with fonts, scripts etc. (missing header, Access-Control-Allow-Origin).

## Conclusion

I am very satisfied with DO's APP-Platform so far. The performance is good and i feel safe. Soon i will test the log forwarding. If there is something to write about it, i will link it here.

---

Thanks for reading.

Originally published at [rburkhardt.com](https://rburkhardt.com/)
Feel free to subscribe to my [RSS](https://rburkhardt.com/feed/) and connect on [Twitter](https://twitter.com/rob_burkhardt) or [Github](https://github.com/rob32)