From bdd1a41f6207275bd24c98136fc567af2ea48b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Behmo?= Date: Mon, 3 Jul 2017 12:39:19 +0200 Subject: [PATCH] :sunrise: --- .gitignore | 2 ++ README.md | 36 +++++++++++++++++++ docker-compose.yml | 53 +++++++++++++++++++++++++++ lms/Dockerfile | 68 +++++++++++++++++++++++++++++++++++ lms/lms.auth.json | 73 +++++++++++++++++++++++++++++++++++++ lms/lms.env.json | 74 ++++++++++++++++++++++++++++++++++++++ lms/production.py | 17 +++++++++ nginx/Dockerfile | 15 ++++++++ nginx/lms.conf | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 427 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 lms/Dockerfile create mode 100644 lms/lms.auth.json create mode 100644 lms/lms.env.json create mode 100644 lms/production.py create mode 100644 nginx/Dockerfile create mode 100644 nginx/lms.conf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6e6e08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.*.swp +data/* diff --git a/README.md b/README.md new file mode 100644 index 0000000..4eee41b --- /dev/null +++ b/README.md @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b12934f --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/lms/Dockerfile b/lms/Dockerfile new file mode 100644 index 0000000..b4c0cd6 --- /dev/null +++ b/lms/Dockerfile @@ -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 diff --git a/lms/lms.auth.json b/lms/lms.auth.json new file mode 100644 index 0000000..dd438e7 --- /dev/null +++ b/lms/lms.auth.json @@ -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" + } + } + ] + } + } +} +} diff --git a/lms/lms.env.json b/lms/lms.env.json new file mode 100644 index 0000000..b1f2d84 --- /dev/null +++ b/lms/lms.env.json @@ -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" + } + } +} diff --git a/lms/production.py b/lms/production.py new file mode 100644 index 0000000..21330cc --- /dev/null +++ b/lms/production.py @@ -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) diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..5977964 --- /dev/null +++ b/nginx/Dockerfile @@ -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 diff --git a/nginx/lms.conf b/nginx/lms.conf new file mode 100644 index 0000000..8ea29a9 --- /dev/null +++ b/nginx/lms.conf @@ -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= can be used by 3rd party sites in 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.*) { + root /openedx/uploads; + try_files /$file =404; + expires 31536000s; + } + + location ~ ^/static/(?P.*) { + 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.*\.[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[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; + } +}