diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..979bf6b132087e22b4d76d2bd03294bf8b5b7134
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,14 @@
+.DS_Store
+.git
+logs
+*.log
+scripts
+pids
+*.pid
+*.seed
+lib-cov
+coverage
+.grunt
+.lock-wscript
+build/Release
+node_modules
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..a614e973b8f137e66ae4c280ba5fc14363fcb4e6
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,29 @@
+FROM spacedeck/docker-baseimage:latest
+ENV NODE_ENV production
+
+RUN mkdir -p /usr/src/app
+WORKDIR /usr/src/app
+
+COPY package.json /usr/src/app/
+RUN npm install
+RUN npm install gulp-rev-replace gulp-clean gulp-fingerprint gulp-rev gulp-rev-all gulp-rev-replace
+RUN npm install -g --save-dev gulp
+
+COPY app.js Dockerfile Gulpfile.js LICENSE /usr/src/app/
+COPY config /usr/src/app/config
+COPY helpers /usr/src/app/helpers
+COPY locales /usr/src/app/locales
+COPY middlewares /usr/src/app/middlewares
+COPY models /usr/src/app/models
+COPY public /usr/src/app/public
+COPY routes /usr/src/app/routes
+COPY styles /usr/src/app/styles
+COPY views /usr/src/app/views
+
+RUN gulp all
+RUN npm cache clean
+
+CMD [ "node", "app.js" ]
+
+EXPOSE 9666
+
diff --git a/Gulpfile.js b/Gulpfile.js
index af47a5dea2b7051fa016a417ac923c0c70b1c24d..e4aa47e717618173a609c3110cf85f3bd418dccb 100644
--- a/Gulpfile.js
+++ b/Gulpfile.js
@@ -12,10 +12,9 @@ var uglify = require('gulp-uglify');
 var fingerprint = require('gulp-fingerprint');
 var rev = require('gulp-rev');
 
-var RevAll = require('gulp-rev-all');
+var revAll = require('gulp-rev-all');
 
 gulp.task('rev', () => {
-  var revAll = new RevAll();
   return gulp.src(['public/**'])
     .pipe(gulp.dest('build/assets'))
     .pipe(revAll.revision())
diff --git a/README.md b/README.md
index 723f75f6015d34c66328a3d77efaf50a0eac081f..60ad14a45d01e8a392f3147436a879240f88fc6d 100644
--- a/README.md
+++ b/README.md
@@ -23,10 +23,10 @@ We appreciate filed issues, pull requests and general discussion.
 
 Spacedeck uses the following major building blocks:
 
+- Vue.js (Frontend)
 - Node.js 7.x (Backend / API)
 - MongoDB 3.x (Datastore)
 - Redis 3.x (Datastore for realtime channels)
-- Vue.js (Frontend)
 
 It also has some binary dependencies for media conversion and PDF export:
 
@@ -50,8 +50,14 @@ see: config/config.json
 
     export NODE_ENV=development
     npm start
+    open http://localhost:9666
+
+# experimental docker(compose) support
+
+We have a docker base image at https://github.com/spacedeck/docker-baseimage that includes all required binaries. Based on this image we can use Docker Compose to bootstrap a Spacedeck including data storage.
 
-    open http://localhost:9000
+    docker-compose build
+    docker-compose run -e ENV=development -p 9666:9666 -e NODE_ENV=development spacedeck-open
 
 # License
 
diff --git a/app.js b/app.js
index a6e67e1ce6bf5dde39d8b017b56c82c19bbc6c9c..004fb5a5bd82f1c73708de91b73b4433fd22dcea 100644
--- a/app.js
+++ b/app.js
@@ -50,7 +50,7 @@ swig.setFilter('cdn', function(input, idx) {
 app.engine('html', swig.renderFile);
 app.set('view engine', 'html');
 
-if (app.get('env') != 'development') {
+if (isProduction) {
   app.set('views', path.join(__dirname, 'build', 'views'));
   app.use(favicon(path.join(__dirname, 'build', 'assets', 'images', 'favicon.png')));
   app.use(express.static(path.join(__dirname, 'build', 'assets')));
@@ -84,7 +84,6 @@ app.use(helmet.noSniff())
 app.use(require("./middlewares/templates"));
 app.use(require("./middlewares/error_helpers"));
 app.use(require("./middlewares/setuser"));
-app.use(require("./middlewares/subdomain"));
 app.use(require("./middlewares/cors"));
 app.use(require("./middlewares/i18n"));
 app.use("/api", require("./middlewares/api_helpers"));
@@ -129,11 +128,11 @@ if (app.get('env') == 'development') {
 module.exports = app;
 
 // CONNECT TO DATABASE
-const mongoHost = process.env.MONGO_PORT_27017_TCP_ADDR || 'localhost';
+const mongoHost = process.env.MONGO_PORT_27017_TCP_ADDR || config.get('mongodb_host');
 mongoose.connect('mongodb://' + mongoHost + '/spacedeck');
 
 // START WEBSERVER
-const port = 9000;
+const port = 9666;
 
 const server = http.Server(app).listen(port, () => {
   
diff --git a/config/default.json b/config/default.json
index 6fd9f66a73a07bdf7b384c6aa6eb6b9f8236dcac..37a00aa80a9248f2d22349a7622ea3e44cfc49cb 100644
--- a/config/default.json
+++ b/config/default.json
@@ -1,9 +1,20 @@
 {
-  "endpoint": "http://localhost:9000",
+  //"endpoint": "http://localhost:9000",
+  "endpoint": "http://localhost:9666",
   "storage_region": "eu-central-1",
+
+  //"storage_bucket": "sdeck-development",
+  //"storage_cdn": "http://localhost:9123/sdeck-development",
+  //"storage_endpoint": "http://storage:9000",
+
   "storage_bucket": "my_spacedeck_bucket",
   "storage_cdn": "/storage",
   "storage_local_path": "./storage",
+
+  "redis_mock": true,
+  "mongodb_host": "localhost",
+  "redis_host": "localhost",
+
   "google_access" : "",
   "google_secret" : "",
   "admin_pass": "very_secret_admin_password",
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..688f381a1c81bef19525054d5dc488f874f58bf4
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,34 @@
+version: '2'
+services:
+  sync:
+    image: redis
+  storage:
+    image: minio/minio
+    environment:
+      - MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
+      - MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
+    ports:
+      - 9123:9000
+    command: server /export
+  db:
+    image: mongo
+  spacedeck-open:
+    environment:
+      - env=development
+      - MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
+      - MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
+    build: .
+    volumes:
+      # - ./:/usr/src/app
+      - /usr/src/app/node_modules
+    command: npm start
+    ports:
+      - 9666:9666
+    depends_on:
+      - db
+      - sync
+      - storage
+    links:
+      - storage
+      - db
+      - sync
diff --git a/helpers/mailer.js b/helpers/mailer.js
index 8da189ddf9e1ae08841d2ad030cb4e9b8c1c3a1f..15545d0d73d6fabe64247af1ca8b41d212ffad48 100644
--- a/helpers/mailer.js
+++ b/helpers/mailer.js
@@ -53,7 +53,7 @@ module.exports = {
           }
         }
       }, function(err, data) {
-        if(err) console.log('Email not sent:', err);
+        if (err) console.error("Error sending email:", err);
         else console.log("Email sent.");
       });
     }
diff --git a/helpers/phantom.js b/helpers/phantom.js
index c195a529c55125f3baed06a8b1eca40adeb81dea..ed897f550205491e1055cce419f4d9e30eb4cccb 100644
--- a/helpers/phantom.js
+++ b/helpers/phantom.js
@@ -32,31 +32,36 @@ module.exports = {
     };
 
     phantom.create({ path: require('phantomjs-prebuilt').path }, function (err, browser) {
-      return browser.createPage(function (err, page) {
-        console.log("page created, opening ",space_url);
+      if(err){
+        console.log(err);
+      }else{
+        return browser.createPage(function (err, page) {
+          console.log("page created, opening ",space_url);
 
-        if (type=="pdf") {
-          var psz = {
-            width: space.advanced.width+"px",
-            height: space.advanced.height+"px"
-          };
-          page.set('paperSize', psz);
-        }
+          if (type=="pdf") {
+            var psz = {
+              width: space.advanced.width+"px",
+              height: space.advanced.height+"px"
+            };
+            page.set('paperSize', psz);
+          }
 
-        page.set('settings.resourceTimeout',timeout);
-        page.set('settings.javascriptEnabled',false);
+          page.set('settings.resourceTimeout',timeout);
+          page.set('settings.javascriptEnabled',false);
 
-        return page.open(space_url, function (err,status) {
-          page.render(export_path, function() {
-            on_success_called = true;
-            if (on_success) {
-              on_success(export_path);
-            }
-            page.close();
-            browser.exit();
+          return page.open(space_url, function (err,status) {
+            page.render(export_path, function() {
+              on_success_called = true;
+              if (on_success) {
+                on_success(export_path);
+              }
+              page.close();
+              browser.exit();
+            });
           });
-        });
-      });
+        });        
+      }
+
     }, {
       onExit: on_exit
     });
diff --git a/helpers/redis.js b/helpers/redis.js
index 9866ba447d7828a22d31e0183089a4b93f9b29bb..69b9411c8c126e3af1cc722d83e4f4e52bd22a66 100644
--- a/helpers/redis.js
+++ b/helpers/redis.js
@@ -1,5 +1,7 @@
 'use strict';
 
+const config = require('config');
+
 // this is a mock version of the Redis API,
 // emulating Redis if it is not available locally
 var notRedis = {
@@ -92,7 +94,12 @@ var notRedis = {
 
 module.exports = {
   connectRedis: function() {
-    this.connection = notRedis;
+    if (config.get("redis_mock")) {
+      this.connection = notRedis;
+    } else {
+      const redisHost = process.env.REDIS_PORT_6379_TCP_ADDR || 'sync';
+      this.connection = new RedisConnection(6379, redisHost);
+    }
   },
   getConnection: function() {
     this.connectRedis();
diff --git a/helpers/uploader.js b/helpers/uploader.js
index 7fe1df14864c6c062208b50ad0ff4613c01e6972..958d12c3de3b14b59512961158b050e17cd6d49a 100644
--- a/helpers/uploader.js
+++ b/helpers/uploader.js
@@ -2,21 +2,40 @@
 
 var fs = require('fs');
 var config = require('config');
+var s3 = null;
 
 // use AWS S3 or local folder depending on config
 if (config.get("storage_local_path")) {
   var AWS = require('mock-aws-s3');
   AWS.config.basePath = config.get("storage_local_path");
+  s3 = new AWS.S3();
 } else {
   var AWS = require('aws-sdk');
-  AWS.config.region = config.get("storage_region");
+  var storage_endpoint = config.get("storage_endpoint");
+  const ep = new AWS.Endpoint(storage_endpoint);
+
+  AWS.config.update(new AWS.Config({
+    accessKeyId: process.env.MINIO_ACCESS_KEY, 
+    secretAccessKey: process.env.MINIO_SECRET_KEY, 
+    region: config.get("storage_region"),
+    s3ForcePathStyle: true,
+    signatureVersion: 'v4'
+  }));
+  s3 = new AWS.S3({
+    endpoint: ep
+  });
 }
 
+s3.createBucket({
+  Bucket: config.get("storage_bucket"),
+  ACL: "public-read",
+  GrantRead: "*"
+}, (err,res) => {
+  console.log("createBucket",err,res);
+});
+
 module.exports = {
   removeFile: (path, callback) => {
-    const s3 = new AWS.S3({
-      region: config.get("storage_region")
-    });
     const bucket = config.get("storage_bucket");
     s3.deleteObject({
       Bucket: bucket, Key: path
@@ -34,7 +53,7 @@ module.exports = {
       callback({error:"missing path"}, null);
       return;
     }
-    console.log("[s3] uploading", localFilePath, " to ", fileName);
+    console.log("[storage] uploading", localFilePath, " to ", fileName);
 
     const bucket = config.get("storage_bucket");
     const fileStream = fs.createReadStream(localFilePath);
@@ -45,10 +64,6 @@ module.exports = {
       }
     });
     fileStream.on('open', function () {
-      var s3 = new AWS.S3({
-        region: config.get("storage_region")
-      });
-
       s3.putObject({
         Bucket: bucket,
         Key: fileName,
@@ -58,7 +73,7 @@ module.exports = {
         if (err){
           console.error(err);
           callback(err);
-        }else {
+        } else {
           const url = config.get("storage_cdn") + "/" + fileName;
           console.log("[s3]" + localFilePath + " to " + url);
           callback(null, url);
diff --git a/helpers/websockets.js b/helpers/websockets.js
index 9394913c946a1ee8ec65fbdd7ff8d4e079da713a..d541ede8cf5e0efde4126880715dc90db96b43cb 100644
--- a/helpers/websockets.js
+++ b/helpers/websockets.js
@@ -1,21 +1,28 @@
 'use strict';
 require('../models/schema');
 
+const config = require('config');
+
 const WebSocketServer = require('ws').Server;
 
+const RedisConnection = require('ioredis');
 const async = require('async');
 const _ = require("underscore");
 const mongoose = require("mongoose");
 const crypto = require('crypto');
 
-var redis = require("./redis.js");
+const redisMock = require("./redis.js");
 
 module.exports = {
   startWebsockets: function(server) {
     this.setupSubscription();
-    this.state = redis.getConnection();
     
-    if(!this.current_websockets) {
+    if (!this.current_websockets) {
+      if (config.get("redis_mock")) {
+        this.state = redisMock.getConnection();
+      } else {
+        this.state = new RedisConnection(6379, process.env.REDIS_PORT_6379_TCP_ADDR || config.get("redis_host"));
+      }
       this.current_websockets = [];
     }
 
@@ -118,9 +125,17 @@ module.exports = {
   },
 
   setupSubscription: function() {
-    this.cursorSubscriber = redis.getConnection().subscribe(['cursors', 'users', 'updates'], function (err, count) {
-      console.log("[redis] websockets to " + count + " topics." );
-    });
+    if (config.get("redis_mock")) {
+      this.cursorSubscriber = redisMock.getConnection().subscribe(['cursors', 'users', 'updates'], function (err, count) {
+        console.log("[redis-mock] websockets subscribed to " + count + " topics." );
+      });
+    } else {
+      this.cursorSubscriber = new RedisConnection(6379, process.env.REDIS_PORT_6379_TCP_ADDR || config.get("redis_host"));
+      this.cursorSubscriber.subscribe(['cursors', 'users', 'updates'], function (err, count) {
+        console.log("[redis] websockets subscribed to " + count + " topics." );
+      });
+    }
+    
     this.cursorSubscriber.on('message', function (channel, rawMessage) {
       const msg = JSON.parse(rawMessage);
       const spaceId = msg.space_id;
diff --git a/middlewares/setuser.js b/middlewares/setuser.js
index 349aebf38704cde03500745696c7505ff17bc7e4..56bcd787a98d221ce80d1298c7f836785ff428fc 100644
--- a/middlewares/setuser.js
+++ b/middlewares/setuser.js
@@ -5,27 +5,24 @@ var config = require('config');
 
 module.exports = (req, res, next) => {
   const token = req.cookies["sdsession"];
+  
   if (token && token != "null" && token !== null) {
     User.findOne({
       "sessions.token": token
     }).populate('team').exec((err, user) => {
+      if (err) console.error("session.token lookup error:",err);
       if (!user) {
-        // FIXME
-        var domain = "localhost";
-        res.clearCookie('sdsession', {
-          domain: domain
-        });
+        res.clearCookie('sdsession');
 
         if (req.accepts("text/html")) {
-          res.redirect("/");
+          res.send("Please clear your cookies and try again.");
         } else if (req.accepts('application/json')) {
           res.status(403).json({
             "error": "token_not_found"
           });
         } else {
-          res.redirect("/");
+          res.send("Please clear your cookies and try again.");
         }
-
       } else {
         req["token"] = token;
         req["user"] = user;
diff --git a/models/team.js b/models/team.js
index a1f3cc7d02de8b44df721727e390efb06880a0bf..b35942c8451e2e9813bdfccdd43d8a4b4b810759 100644
--- a/models/team.js
+++ b/models/team.js
@@ -45,7 +45,7 @@ module.exports.teamSchema.index({
 
 module.exports.teamSchema.statics.getTeamForHost = (host, cb) => {
 
-  if (host != "127.0.0.1:9000") { //phantomjs check
+  if (host != "127.0.0.1:9666") { //phantomjs check
     let subDomainParts = host.split('.');
 
     if (subDomainParts.length > 2) {
diff --git a/routes/api/sessions.js b/routes/api/sessions.js
index 3b6304265065aea0d3e6a0945e9f1f716197f918..054e49ce27032828269bdee802d2001204a93ebb 100644
--- a/routes/api/sessions.js
+++ b/routes/api/sessions.js
@@ -5,6 +5,7 @@ require('../../models/schema');
 
 var bcrypt = require('bcryptjs');
 var crypo = require('crypto');
+var URL = require('url').URL;
 
 var express = require('express');
 var router = express.Router();
@@ -40,11 +41,11 @@ router.post('/', function(req, res) {
               user.sessions.push(session);
 
               user.save(function(err, result) {
-                // FIXME
-                var secure = process.env.NODE_ENV == "production" || process.env.NODE_ENV == "staging";
-                var domain = (process.env.NODE_ENV == "production") ? ".example.org" : "localhost";
+                if (err) console.error("Error saving user:",err);
+                
+                var domain = (process.env.NODE_ENV == "production") ? new URL(config.get('endpoint')).hostname : "localhost";
 
-                res.cookie('sdsession', token, { domain: domain, httpOnly: true, secure: secure});
+                res.cookie('sdsession', token, { domain: domain, httpOnly: true });
                 res.status(201).json(session);
               });
             });
@@ -69,8 +70,7 @@ router.delete('/current', function(req, res, next) {
     });
     user.sessions = newSessions;
     user.save(function(err, result) {
-      // FIXME
-      var domain = (process.env.NODE_ENV == "production") ? ".example.org" : "localhost";
+      var domain = new URL(config.get('endpoint')).hostname;
       res.clearCookie('sdsession', { domain: domain });
       res.sendStatus(204);
     });
diff --git a/routes/api/users.js b/routes/api/users.js
index d8e995caef9ce55ca5ba6e21e5c73dc132525271..7e1a1aa0287da723caab962e023cebd079b01c70 100644
--- a/routes/api/users.js
+++ b/routes/api/users.js
@@ -16,6 +16,7 @@ var fs = require('fs');
 var request = require('request');
 var gm = require('gm');
 var validator = require('validator');
+var URL = require('url').URL;
 
 var express = require('express');
 var router = express.Router();
@@ -182,8 +183,7 @@ router.get('/loginorsignupviagoogle', function(req, res) {
       var apiUrl = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=" + tokens.access_token;
       
       var finalizeLogin = function(session){
-        var secure = process.env.NODE_ENV == "production" || process.env.NODE_ENV == "staging";
-        res.cookie('sdsession', session.token, { httpOnly: true, secure: secure});
+        res.cookie('sdsession', session.token, { httpOnly: true });
         res.status(201).json(session);
       };
 
diff --git a/views/layouts/outer.html b/views/layouts/outer.html
index 875358225577c451bd5ea37d6c315ac5be21726a..603d96a1076339139518dfe7226ed4fceeeb6927 100644
--- a/views/layouts/outer.html
+++ b/views/layouts/outer.html
@@ -53,7 +53,7 @@
       <p>
         <div class="col-xs-6">
           <a href="/contact">[[ __("contact") ]]</a>
-          <span style="color:#888">&copy; 2011–2017 The Spacedeck Open Developers</span>
+          <span style="color:#888">&copy; 2011–2018 The Spacedeck Open Developers <a href="https://github.com/spacedeck/spacedeck-open">https://github.com/spacedeck/spacedeck-open</a></span>
         </div>
       </p>
     </div>
diff --git a/views/spacedeck.html b/views/spacedeck.html
index 54144dd81cb6ddd1fec36e47d8ae63e8f3a7c42c..8cf7ec4eec52b7fcb713631399431e98ef7e2cf0 100644
--- a/views/spacedeck.html
+++ b/views/spacedeck.html
@@ -23,10 +23,18 @@
       {% if process.env.NODE_ENV != "production" %}
         var ENV = {
           name: 'development',
-          webHost: "localhost:9000",
-          webEndpoint:"http://localhost:9000",
-          apiEndpoint: "http://localhost:9000",
-          websocketsEndpoint: "ws://localhost:9000"
+          webHost: "localhost:9666",
+          webEndpoint:"http://localhost:9666",
+          apiEndpoint: "http://localhost:9666",
+          websocketsEndpoint: "ws://localhost:9666"
+        };
+      {% else %}
+        var ENV = {
+          name: 'production',
+          webHost: location.host,
+          webEndpoint: location.origin,
+          apiEndpoint: location.origin,
+          websocketsEndpoint: location.origin.replace("https:","wss:").replace("http:","ws:")
         };
       {% endif %}