Deploy Django App with Channels (WebSocket, ASGI)

I am new to Fly.io. I want to deploy a Django app that implements Channels for WebSockets. Is there a good guide for doing so? I used Daphne for the development, but is Daphne available on Fly.io or should I think about Uvicorn? Or should I put all of this in Docker and deploy that way? Sorry for the newbie question, trying to sort my way through the overall plan. Any advice would certainly be appreciated.

I’m not personally aware of a solution here but you might try the Django Forum where people who would know hang out. I’m not sure it’s a dependent answer.

Thank you. I will check that out. I appreciate the response.

1 Like

I’m curious what you discover. Daphne looks like it’ll work on Fly. I think you may be able to tweak the Dockerfile flyctl generates when you run fly launch. It uses gunicorn by default, but the CMD line could change to start Daphne instead.

Thanks. I still need to read and understand flyctl better. Newbie here. :slight_smile:

Try flyctl launch on your app and see what happens. It’s pretty good at Django thanks to @wsvincent up there. :wink:

1 Like

I got the ASGI-WebSocket app to deploy. Live app is at: https://bitter-fire-7529.fly.dev/ .

Following are thoughts on deployment.

First, I had a Django/React app implementing WebSockets that worked locally. You can see the working code here: GitHub - TKChattoraj/websocket_functionality: Simple app to demonstrate WebSocket functionality. I won’t discuss what is needed to create a working local development, except to note the the Requirements file. Among others, Daphne and Whitenoise are needed.

I ran through the normal deployment instructions, basically following the Django Guide: Getting Started · Fly Docs, allowing automatic creation of the Dockerfile. Then I revised the CMD entry in the Dockerfile:

CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]

Note: I for some reason, I don’t think this is the way to do it for real in production, but it worked for me and so I am going with it for now. I think a possible solution will be to make a script that

  1. Starts Daphne
  2. Starts python manage.py runworker

The CMD entry wold be to then run that script. I’d be interested in anyone’s comments about this.

I encountered an “[Failed to construct ‘WebSocket’: An insecure WebSocket connection may not be initiated from a page loaded over HTTPS].” I solved that using the following: node.js - React app error: Failed to construct 'WebSocket': An insecure WebSocket connection may not be initiated from a page loaded over HTTPS - Stack Overflow . The solution was to replace “ws://…” in the app WebSocket creation to “wss://…”

I ran into two other categories of errors, Static Files and Redis. These don’t really seem to be related to the ASGI nature of the app, but I experienced them here so I’ll write about them here.

Static Files
To handle the static files, the settings.py file contains the following:

WHITENOISE_MIMETYPES = {'.js': 'text/javascript'}
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    ...
]
...

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR/"staticfiles"
# Extra places for collectstatic to find static files.
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

A couple of things to point out:

  1. WHITENOISE_MIMETYPES = {'.js': 'text/javascript'} is done to deal with MIMETYPE explained here: Resource blocked due to MIME type mismatch .

  2. Some good info on dealing with static files is explained here: Managing static files (e.g. images, JavaScript, CSS) | Django documentation | Django .

Redis
I understand Redis’s role here is as a message queue. It implements the Channel Layer and keeps the messages arriving from the Client for when the app might later need to deal with them.

In settings.py:

from urllib.parse import urlparse
# code from https://stackoverflow.com/questions/62777377/long-url-including-a-key-causes-unicode-idna-codec-decoding-error-whilst-using
def parse_redis_url(url):
    """ parses a redis url into component parts, stripping password from the host.
    Long keys in the url result in parsing errors, since labels within a hostname cannot exceed 64 characters under
    idna rules.
    In that event, we remove the key/password so that it can be passed separately to the RedisChannelLayer.
    """
    parsed = urlparse(url)
    parts = parsed.netloc.split(':')
    host = ':'.join(parts[0:-1])
    port = parts[-1]
    path = parsed.path.split('/')[1:]
    db = int(path[0]) if len(path) >= 1 else 0

    user, password = (None, None)
    if '@' in host:
        creds, host = host.split('@')
        user, password = creds.split(':')
        host = f'{user}@{host}'

    return host, port, user, password, db

REDIS_URL = os.environ.get('REDIS_URL', default='redis://localhost:6379')
REDIS_HOST, REDIS_PORT, REDIS_USER, REDIS_PASSWORD, REDIS_DB = parse_redis_url(REDIS_URL)


CHANNEL_LAYERS={
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [{
                'address': f'redis://{REDIS_HOST}:{REDIS_PORT}',
                'db': REDIS_DB,
                'password': REDIS_PASSWORD,
            }],
        },
    },
}

After running flyctl deploy, I got the Redis url. See Redis by Upstash · Fly Docs . Then I ran the following in flyctl:

flyctl secrets set REDIS_URL=redis://long_ass_url_redis.upstash.io

As I understand it, this command will override the values specified in settings.py. Note the method to parse the REDIS_URL. I didn’t write it. It came from:
sockets - Long URL (including a key) causes unicode idna codec decoding error whilst using RedisChannelLayer in Django Channels - Stack Overflow . Parsing the REDIS_URL is needed to avoid an "unicode idna codec decoding error whilst using RedisChannelLayer in Django Channels. Again see the Stackoverflow link above.

With that my code deployed properly. Definitely interested in feedback to make this more efficient or to properly implement things that might be a hack.