This commit is contained in:
Régis Behmo 2017-07-03 12:39:19 +02:00
commit bdd1a41f62
9 changed files with 427 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.*.swp
data/*

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# [WIP] Docker-compose Open edX production stack
This is a work-in-progress.
The production stack is composed of Nginx, MySQL, MongoDB, Memcache and an LMS container.
## Install
Create necessary data folders:
mkdir -p data/lms/course_static data/lms/data data/lms/logs data/lms/staticfiles data/lms/uploads
mkdir -p data/mongodb data/mysql
## Lauch a production stack
docker-compose up --build
Container data is in `./data`.
## Development tips & tricks
Open a bash in the lms:
docker-compose run lms bash
How to find the IP address of a running docker:
docker container ls
docker inspect a0fc4cc602f8
## TODO
- Add a CMS container
- Add rabbitmq and celery worker containers
- Make sure that secret keys are not shared with the entire world
- Fix TODOs

53
docker-compose.yml Normal file
View File

@ -0,0 +1,53 @@
version: "3"
services:
memcached:
image: memcached:1.4.38
mongodb:
# Use WiredTiger in all environments, just like at edx.org
command: mongod --smallfiles --nojournal --storageEngine wiredTiger
image: mongo:3.0.14
volumes:
- ./data/mongodb:/data/db
mysql:
image: mysql:5.6.36
command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci
environment:
MYSQL_ROOT_PASSWORD: ""
MYSQL_DATABASE: "openedx"
MYSQL_USER: "openedx"
MYSQL_PASSWORD: "password"
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
volumes:
- ./data/mysql:/var/lib/mysql
nginx:
build: ./nginx
volumes:
- ./data/lms/course_static:/openedx/course_static:ro
- ./data/lms/staticfiles:/openedx/staticfiles:ro
- ./data/lms/uploads:/openedx/uploads:ro
depends_on:
- lms
lms:
build:
context: ./lms
args:
RUN_MIGRATIONS: 1
COLLECT_STATIC: 1
volumes:
- ./data/lms/course_static:/openedx/course_static
- ./data/lms/data:/openedx/data
- ./data/lms/logs:/openedx/logs
- ./data/lms/staticfiles:/openedx/staticfiles
- ./data/lms/uploads:/openedx/uploads
depends_on:
- memcached
- mongodb
- mysql
# TODO rabbitmq and celery workers

68
lms/Dockerfile Normal file
View File

@ -0,0 +1,68 @@
FROM ubuntu:16.04
# Install system requirements
RUN apt update
RUN apt upgrade -y
# Global requirements
RUN apt install -y language-pack-en git python-virtualenv build-essential software-properties-common curl git-core libxml2-dev libxslt1-dev python-pip libmysqlclient-dev python-apt python-dev libxmlsec1-dev libfreetype6-dev swig gcc g++
# lms requirements
RUN apt install -y gettext gfortran graphviz graphviz-dev libffi-dev libfreetype6-dev libgeos-dev libjpeg8-dev liblapack-dev libpng12-dev libxml2-dev libxmlsec1-dev libxslt1-dev nodejs npm ntp pkg-config
# Install symlink so that we have access to 'node' binary without virtualenv.
# This replaces the "nodeenv" install.
RUN apt install -y nodejs-legacy
# Create necessary folders
RUN mkdir /openedx
RUN mkdir /openedx/data
RUN mkdir /openedx/logs
RUN mkdir /openedx/uploads
RUN mkdir /openedx/staticfiles
VOLUME /openedx/data
VOLUME /openedx/logs
VOLUME /openedx/staticfiles
VOLUME /openedx/uploads
WORKDIR /openedx
# Checkout edx-platform code
RUN git clone https://github.com/edx/edx-platform.git
WORKDIR /openedx/edx-platform
RUN git checkout open-release/ficus.master
# Install python requirements
RUN pip install pip==8.1.2
RUN pip install setuptools==24.0.3
RUN pip install -r requirements/edx/pre.txt
RUN pip install -r requirements/edx/github.txt
RUN pip install -r requirements/edx/local.txt
RUN pip install -r requirements/edx/base.txt
RUN pip install -r requirements/edx/post.txt
RUN pip install -r requirements/edx/paver.txt
# Finish requirements install
RUN paver install_prereqs
# Copy configuration files
COPY ./lms.env.json /openedx/
COPY ./lms.auth.json /openedx/
COPY ./production.py /openedx/edx-platform/lms/envs/
# Configure environment
ENV DJANGO_SETTINGS_MODULE lms.envs.production
ENV SERVICE_VARIANT lms
# Run server
EXPOSE 8000
# Migrate
ARG RUN_MIGRATIONS=0
ENV RUN_MIGRATIONS ${RUN_MIGRATIONS}
# Collect static assets
ARG COLLECT_STATIC=0
ENV COLLECT_STATIC ${COLLECT_STATIC}
# TODO Here we wait until mysql and mongodb become available but it's a terrible solution
CMD sleep 5 && \
if [ "$RUN_MIGRATIONS" = "1" ] ; then ./manage.py lms migrate --settings=production ; fi && \
if [ "$COLLECT_STATIC" = "1" ] ; then paver update_assets lms --settings=production ; fi && \
gunicorn --name lms --bind=0.0.0.0:8000 --max-requests=1000 lms.wsgi:application

73
lms/lms.auth.json Normal file
View File

@ -0,0 +1,73 @@
{
"SECRET_KEY": "7i#nri2i@--brp0sri9qf@ewlj1qxghv0%af$sk4ntn9pv$8t#",
"AWS_ACCESS_KEY_ID": "",
"AWS_SECRET_ACCESS_KEY": "",
"XQUEUE_INTERFACE": {
"basic_auth": ["edx", "edx"],
"django_auth": {
"username": "lms",
"password": "password"
},
"url": "http://localhost:18040"
},
"CONTENTSTORE": {
"ENGINE": "xmodule.contentstore.mongo.MongoContentStore",
"DOC_STORE_CONFIG": {
"db": "edxapp",
"host": "mongodb"
}
},
"DOC_STORE_CONFIG": {
"db": "edxapp",
"host": "mongodb"
},
"DATABASES": {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "openedx",
"USER": "openedx",
"PASSWORD": "password",
"HOST": "mysql",
"PORT": "3306",
"ATOMIC_REQUESTS": true
}
},
"MODULESTORE": {
"default": {
"ENGINE": "xmodule.modulestore.mixed.MixedModuleStore",
"OPTIONS": {
"mappings": {},
"stores": [
{
"NAME": "split",
"ENGINE": "xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore",
"DOC_STORE_CONFIG": {
"host": "mongodb",
"db": "xmodule",
"collection": "modulestore"
},
"OPTIONS": {
"default_class": "xmodule.hidden_module.HiddenDescriptor",
"fs_root": "/openedx/data",
"render_template": "edxmako.shortcuts.render_to_string"
}
},
{
"NAME": "draft",
"ENGINE": "xmodule.modulestore.mongo.DraftMongoModuleStore",
"DOC_STORE_CONFIG": {
"host": "mongodb",
"db": "xmodule",
"collection": "modulestore"
},
"OPTIONS": {
"default_class": "xmodule.hidden_module.HiddenDescriptor",
"fs_root": "/opt/openedx/data",
"render_template": "edxmako.shortcuts.render_to_string"
}
}
]
}
}
}
}

74
lms/lms.env.json Normal file
View File

@ -0,0 +1,74 @@
{
"SITE_NAME": "myopenedx.com",
"BOOK_URL": "",
"LOG_DIR": "/openedx/logs",
"LOGGING_ENV": "sandbox",
"OAUTH_OIDC_ISSUER": "http://localhost:8000/oauth2",
"PLATFORM_NAME": "My Open edX",
"FEATURES": {
"PREVIEW_LMS_BASE": "localhost:8000"
},
"LMS_ROOT_URL": "http://myopenedx.com",
"CMS_ROOT_URL": "http://studio.myopenedx.com",
"CMS_BASE": "studio.myopenedx.com",
"LMS_BASE": "myopenedx.com",
"CELERY_BROKER_HOSTNAME": "localhost",
"CELERY_BROKER_TRANSPORT": "amqp",
"MEDIA_ROOT": "/openedx/uploads/",
"CACHES": {
"default": {
"KEY_PREFIX": "default",
"VERSION": "1",
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"KEY_FUNCTION": "util.memcache.safe_key",
"KEY_PREFIX": "default",
"LOCATION": "memcached:11211"
},
"general": {
"KEY_PREFIX": "general",
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"KEY_FUNCTION": "util.memcache.safe_key",
"KEY_PREFIX": "default",
"LOCATION": "memcached:11211"
},
"mongo_metadata_inheritance": {
"KEY_PREFIX": "mongo_metadata_inheritance",
"TIMEOUT": 300,
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"KEY_FUNCTION": "util.memcache.safe_key",
"KEY_PREFIX": "default",
"LOCATION": "memcached:11211"
},
"staticfiles": {
"KEY_PREFIX": "staticfiles_general",
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"KEY_FUNCTION": "util.memcache.safe_key",
"KEY_PREFIX": "default",
"LOCATION": "memcached:11211"
},
"configuration": {
"KEY_PREFIX": "configuration",
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"KEY_FUNCTION": "util.memcache.safe_key",
"KEY_PREFIX": "default",
"LOCATION": "memcached:11211"
},
"celery": {
"KEY_PREFIX": "celery",
"TIMEOUT": "7200",
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"KEY_FUNCTION": "util.memcache.safe_key",
"KEY_PREFIX": "default",
"LOCATION": "memcached:11211"
},
"course_structure_cache": {
"KEY_PREFIX": "course_structure",
"LOCATION": "memcached:11211",
"TIMEOUT": "7200",
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"KEY_FUNCTION": "util.memcache.safe_key",
"KEY_PREFIX": "default",
"LOCATION": "memcached:11211"
}
}
}

17
lms/production.py Normal file
View File

@ -0,0 +1,17 @@
from .aws import *
MEDIA_ROOT = "/openedx/uploads/"
FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
ALLOWED_HOSTS = [
'*',
ENV_TOKENS.get('LMS_BASE'),
FEATURES['PREVIEW_LMS_BASE'],
]
# We need to activate dev_env for logging, otherwise rsyslog is required (but
# it is not available in docker).
LOGGING = get_logger_config(LOG_DIR,
logging_env=ENV_TOKENS['LOGGING_ENV'],
debug=False,
dev_env=True,
service_variant=SERVICE_VARIANT)

15
nginx/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM nginx:1.13
RUN mkdir /openedx
RUN mkdir /openedx/course_static
RUN mkdir /openedx/static_files
RUN mkdir /openedx/uploads
VOLUME /openedx/course_static
VOLUME /openedx/staticfiles
VOLUME /openedx/uploads
# Wait until LMS becomes available
# TODO we shouldn't have to wait
RUN sleep 10
COPY lms.conf /etc/nginx/conf.d/lms.conf

89
nginx/lms.conf Normal file
View File

@ -0,0 +1,89 @@
upstream lms-backend {
server lms:8000 fail_timeout=0;
}
server {
listen 80;
server_name myopenedx.com;
# Prevent invalid display courseware in IE 10+ with high privacy settings
add_header P3P 'CP="Open edX does not have a P3P policy."';
# Nginx does not support nested condition or or conditions so
# there is an unfortunate mix of conditonals here.
client_max_body_size 4M;
rewrite ^(.*)/favicon.ico$ /static/images/favicon.ico last;
# Disables server version feedback on pages and in headers
server_tokens off;
location @proxy_to_lms_app {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://lms-backend;
}
location / {
try_files $uri @proxy_to_lms_app;
}
# /login?next=<any image> can be used by 3rd party sites in <img> tags to
# determine whether a user on their site is logged into edX.
# The most common image to use is favicon.ico.
location /login {
if ( $arg_next ~* "favicon.ico" ) {
return 403;
}
try_files $uri @proxy_to_lms_app;
}
# Need a separate location for the image uploads endpoint to limit upload sizes
location ~ ^/api/profile_images/[^/]*/[^/]*/upload$ {
try_files $uri @proxy_to_lms_app;
client_max_body_size 1049576;
}
location ~ ^/media/(?P<file>.*) {
root /openedx/uploads;
try_files /$file =404;
expires 31536000s;
}
location ~ ^/static/(?P<file>.*) {
root /openedx;
try_files /staticfiles/$file /course_static/$file =404;
# return a 403 for static files that shouldn't be
# in the staticfiles directory
location ~ ^/static/(?:.*)(?:\.xml|\.json|README.TXT) {
return 403;
}
# Set django-pipelined files to maximum cache time
location ~ "/static/(?P<collected>.*\.[0-9a-f]{12}\..*)" {
expires max;
# Without this try_files, files that have been run through
# django-pipeline return 404s
try_files /staticfiles/$collected /course_static/$collected =404;
}
# Set django-pipelined files for studio to maximum cache time
location ~ "/static/(?P<collected>[0-9a-f]{7}/.*)" {
expires max;
# Without this try_files, files that have been run through
# django-pipeline return 404s
try_files /staticfiles/$collected /course_static/$collected =404;
}
# Expire other static files immediately (there should be very few / none of these)
expires epoch;
}
}