pythondjangogunicorndigital-oceandjango-imagekit

Django, Gunicorn, Nginx, Postgres, Digitial Ocean Server Error 500 on Image Upload


I am developing a website/blog using Django and am in the middle of figuring out the proper setup/settings. I am running Ubuntu Server 16.04 in a virtual machine for testing. I am using what seems to be a common setup with Gunicorn and Nginx as well as PostgreSQL for the database and hosting the static and media files on Digital Ocean Spaces. I intend to host the site on Digital Ocean as well.

I have pieced together things from a few different guides here, here, here, and here.

I also use Django-Imagekit for handling images (url, resizing, etc) and manage everything in Django Admin.

The problem I am facing is that when I upload an image (either directly to an image form or via a post form) and save the object I end up getting a Server Error (500). If I refresh the page it then works fine. This also happens on the site itself (i.e. go to home page, server error, refresh, no error).

There are also absolutely no errors in my Gunicorn and Nginx logs.

File Structure:

site
├── project
│   ├── gallery
│   │   ├── static
│   │   │   ├── gallery
│   │   │   │   ├── css
│   │   │   │   └── images
│   │   ├── templates
│   │   │   └── gallery
│   │   ├── admin.py
│   │   ├── models.py
│   │   ├── urls.py
│   │   └── views.py
│   ├── posts
│   │   ├── static
│   │   │   ├── posts
│   │   │   │   ├── css
│   │   │   │   └── images
│   │   ├── templates
│   │   │   └── gallery
│   │   ├── admin.py
│   │   ├── models.py
│   │   ├── urls.py
│   │   └── views.py
│   ├── project
│   │   ├── settings
│   │   │   ├── base.py
│   │   │   ├── development.py
│   │   │   ├── local.py
│   │   │   ├── production.py
│   │   │   └── testing.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── static
│   └── templates

gallery/models.py:

...
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit, ResizeToFill
...

class Watermark(object):
    def process(self, image):
        pass

class Image(models.Model):
    original = ImageField(upload_to='images/%Y/%m/%d/')
    large = ImageSpecField(source='original', processors=[Watermark(), \
        ResizeToFit(width=2000, height=2000, upscale=False)], \
        format='JPEG', options={'quality': 90})    
    medium=...
    small=...
    wide=...
    home=...
    upload_date = models.DateTimeField(null=True, editable=False)

    def save(self):
    if not self.id and not self.original:
        return

    if self.upload_date is None:
        self.upload_date = timezone.now()

    image = PIL.Image.open(self.original)
    imgFormat = image.format
    MAX_HEIGHT = 4000
    MAX_WIDTH = 4000

    # Resize image if over MAX pixels in either direction
    (width, height) = image.size
    if height > MAX_HEIGHT or width > MAX_WIDTH:
        ratio = width / height
        output = BytesIO()

        if width > height:
            width = MAX_WIDTH
            height = int(width / ratio)
        else:
            height = MAX_HEIGHT
            width = int(height * ratio)

        size = (width, height)
        image = image.resize(size, PIL.Image.ANTIALIAS)
        image.save(output, format=imgFormat, quality=100)
        self.original = InMemoryUploadedFile(output, 'ImageField', \
            self.original.name, 'images/', sys.getsizeof(output), None)
    super(Image, self).save()

posts/models.py:

class Post(models.Model):
    title = models.CharField(max_length=75)
    ...
    image = models.ForeignKey(Image, blank=True, null=True, \
        on_delete=models.SET_NULL)
    ...

settings/base.py:

import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

SECRET_KEY = os.environ['SECRET_KEY']

DEBUG = True

ALLOWED_HOSTS = []

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'posts',
    'gallery',
    'taggit',
    'ckeditor',
    'storages',
    'imagekit',
    ...
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'project.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'django.template.context_processors.media',
            ],
        },
    },
]

WSGI_APPLICATION = 'project.wsgi.application'

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

settings/testing.py:

from project.settings.base import *

# Override base.py settings here

DEBUG = False

ALLOWED_HOSTS = ['*']

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'test',
        'USER': 'user',
        'PASSWORD': '****',
        'HOST': 'localhost',
        'PORT': '1234',
    }
}

# DigitalOcean Spaces Settings
AWS_ACCESS_KEY_ID = '*****'
AWS_SECRET_ACCESS_KEY = '*****'
AWS_STORAGE_BUCKET_NAME = 'production-storage'
AWS_S3_ENDPOINT_URL = 'https://nyc3.digitaloceanspaces.com'
AWS_S3_OBJECT_PARAMETERS = {
    'CacheControl': 'max-age=86400'
}
AWS_LOCATION = 'static_test/'

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static', 'static'),
]

STATIC_URL = 'https://%s/%s/' % (AWS_S3_ENDPOINT_URL, AWS_LOCATION)
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.NonValidatingImageCacheBackend'

# Needed for CKEditor to work
AWS_QUERYSTRING_AUTH = False

venv/bin/gunicorn_start:

#!/bin/bash

NAME="project"
DIR=/home/user/site/project
USER=brandon
GROUP=brandon
WORKERS=3
BIND=unix:/home/user/run/gunicorn.sock
DJANGO_SETTINGS_MODULE=project.settings.testing
DJANGO_WSGI_MODULE=project.wsgi
SECRET_KEY='*****'
LOG_LEVEL=error

cd $DIR
source ../../venv/bin/activate

export SECRET_KEY=$SECRET_KEY
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DIR:$PYTHONPATH

exec ../../venv/bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
 --name $NAME \
 --workers $WORKERS \
 --user=$USER \
 --group=$GROUP \
 --bind=$BIND \
 --log-level=$LOG_LEVEL \
 --log-file=-

/etc/nginx/sites-available/project:

upstream app_server {
    server unix:/home/user/run/gunicorn.sock fail_timeout=0;
}

server {
    listen 80;

    # add here the ip address of your server
    # or a domain pointing to that ip(like example.com or www.example.com)
    server_name 192.168.1.179

    keepalive_timeout 5;
    client_max_body_size 4G;
    access_log /home/user/logs/nginx-access.log;
    error_log /home/user/logs/nginx-error.log;

    location /static/ {
        alias /home/user/site/project/static;   
    }

    # checks for static file, if not found proxy to app
    location / {
        try_files $uri @proxy_to_app;
    }

    location @proxy_to_app {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://app_server;
    }
}

Any help would be appreciated.

Edit:

Setting Debug to True gave me the following errors.

The issue seems to be related to Django-Imagekit

Internal Server Error: /posts/
Traceback (most recent call last):
    File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 882, in _resolve_lookup
    current = current[bit]
TypeError: 'ImageCacheFile' object is not subscriptable

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/user/venv/lib/python3.5/site-packages/django/core/handlers/exception.py", line 41, in inner
    response = get_response(request)
  File "/home/user/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/user/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/user/site/project/posts/views.py", line 39, in posts
    return render(request, 'posts/posts.html', {'posts':posts, 'recentTags':recent_tags})
  File "/home/user/venv/lib/python3.5/site-packages/django/shortcuts.py", line 30, in render
    content = loader.render_to_string(template_name, context, request, using=using)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/loader.py", line 68, in render_to_string
    return template.render(context, request)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/backends/django.py", line 66, in render
    return self.template.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 207, in render
    return self._render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 199, in _render
    return self.nodelist.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 990, in render
    bit = node.render_annotated(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 957, in render_annotated
    return self.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/loader_tags.py", line 177, in render
    return compiled_parent._render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 199, in _render
    return self.nodelist.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 990, in render
    bit = node.render_annotated(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 957, in render_annotated
    return self.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/loader_tags.py", line 72, in render
    result = block.nodelist.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 990, in render
    bit = node.render_annotated(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 957, in render_annotated
    return self.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/loader_tags.py", line 216, in render
    return template.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 209, in render
    return self._render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 199, in _render
    return self.nodelist.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 990, in render
    bit = node.render_annotated(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 957, in render_annotated
    return self.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/defaulttags.py", line 216, in render
    nodelist.append(node.render_annotated(context))
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 957, in render_annotated
    return self.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/defaulttags.py", line 322, in render
    return nodelist.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 990, in render
    bit = node.render_annotated(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 957, in render_annotated
    return self.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 1040, in render
    output = self.filter_expression.resolve(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 708, in resolve
    obj = self.var.resolve(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 849, in resolve
    value = self._resolve_lookup(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 890, in _resolve_lookup
    current = getattr(current, bit)
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/__init__.py", line 85, in url
    return self._storage_attr('url')
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/__init__.py", line 75, in _storage_attr
    existence_required.send(sender=self, file=self)
  File "/home/user/venv/lib/python3.5/site-packages/django/dispatch/dispatcher.py", line 193, in send
    for receiver in self._live_receivers(sender)
  File "/home/user/venv/lib/python3.5/site-packages/django/dispatch/dispatcher.py", line 193, in <listcomp>
    for receiver in self._live_receivers(sender)
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/registry.py", line 53, in existence_required_receiver
    self._receive(file, 'on_existence_required')
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/registry.py", line 61, in _receive
    call_strategy_method(file, callback)
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/utils.py", line 166, in call_strategy_method
    fn(file)
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/strategies.py", line 15, in on_existence_required
    file.generate()
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/__init__.py", line 94, in generate
    self.cachefile_backend.generate(self, force)
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/backends.py", line 109, in generate
    self.generate_now(file, force=force)
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/backends.py", line 96, in generate_now
    file._generate()
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/__init__.py", line 103, in _generate
    content.seek(0)
ValueError: I/O operation on closed file.

Solution

  • Seems that I managed to find a workaround. The issue is that django-storages was closing the image file after it was uploaded causing an I/O error in django-imagekit.

    I found the workaround here.

    import os
    from storages.backends.s3boto3 import S3Boto3Storage
    from tempfile import SpooledTemporaryFile
    
    class CustomS3Boto3Storage(S3Boto3Storage):
    """
    This is our custom version of S3Boto3Storage that fixes a bug in boto3 where the passed in file is closed upon upload.
    
    https://github.com/boto/boto3/issues/929
    https://github.com/matthewwithanm/django-imagekit/issues/391
    """
    
    def _save_content(self, obj, content, parameters):
        """
        We create a clone of the content file as when this is passed to boto3 it wrongly closes
        the file upon upload where as the storage backend expects it to still be open
        """
        # Seek our content back to the start
        content.seek(0, os.SEEK_SET)
    
        # Create a temporary file that will write to disk after a specified size
        content_autoclose = SpooledTemporaryFile()
    
        # Write our original content into our copy that will be closed by boto3
        content_autoclose.write(content.read())
    
        # Upload the object which will auto close the content_autoclose instance
        super(CustomS3Boto3Storage, self)._save_content(obj, content_autoclose, parameters)
    
        # Cleanup if this is fixed upstream our duplicate should always close        
        if not content_autoclose.closed:
            content_autoclose.close()
    

    Create a file somewhere in the project and add the code (i.e. storage_backends.py). Then in settings, set:

    DEFAULT_FILE_STORAGE='project.storage_backends.CustomS3Boto3Storage'