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.
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'