├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── bin.js ├── index.js ├── package.json └── web ├── bundle.js ├── index.html └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | from mafintosh/node:0.10.32 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-browser-server 2 | 3 | Spawn and expose docker containers over http and websockets 4 | 5 | ``` 6 | npm install -g docker-browser-server 7 | ``` 8 | 9 | Running the above will expose a command line tool called `docker-browser-server`. 10 | To run the server do 11 | 12 | ``` 13 | docker-browser-server docker-image-name --port 8080 14 | ``` 15 | 16 | Make sure you pulled `docker-image-name` from a docker registry first. 17 | Running the above will start the server on port 8080 18 | 19 | Visit http://localhost:8080 after to attach to a container using a web browser. 20 | 21 | You can also pass `--hostNetworking` to run the containers in `host` network mode, meaning the network is not virtualized (may be necessary for p2p connections to work) 22 | 23 | ## Spawn a new container 24 | 25 | To spawn a new container create a websocket to the server and pipe it to a [docker-browser-console](https://github.com/mafintosh/docker-browser-console) instance 26 | 27 | ``` js 28 | // using browserify 29 | var docker = require('docker-browser-console') 30 | var websocket = require('websocket-stream') 31 | 32 | var container = docker() 33 | var socket = websocket('ws://localhost:8080') 34 | 35 | container.appendTo(document.body) 36 | 37 | socket.pipe(container).pipe(socket) 38 | ``` 39 | 40 | A demo interface is available if you simply visit http://localhost:8080 after starting the server 41 | 42 | ## Expose filesystem 43 | 44 | If you run [expose-fs](https://github.com/mafintosh/expose-fs) inside your container the containers 45 | file system will be exposed using the following rest api 46 | 47 | * `GET /files/{container}/{path}` Get a file or directory listing 48 | * `PUT /files/{container}/{path}` Write a new file with the http body 49 | * `POST /files/{container}/{path}` Create a new directory 50 | 51 | ## Expose a http server 52 | 53 | When you spawn a new container a special env var called `$HOST` will be set to a http address. 54 | If you listen on port 80 inside the container all requests going to `$HOST` will be forwarded to this server. 55 | 56 | ## Adventure time 57 | 58 | Checkout [maxogden/adventure-time](https://github.com/maxogden/adventure-time) for a complete client environment 59 | that integrates with docker-browser-server. 60 | 61 | The easiest way to create your own workshop is creating a new Dockerfile and inheriting from the [mafintosh/docker-browser-adventure](https://github.com/mafintosh/docker-adventure-time) image. 62 | This will setup expose-fs and install node among other things 63 | 64 | ``` 65 | FROM mafintosh/docker-adventure-time 66 | RUN npm install -g your-workshop-stuff 67 | ``` 68 | 69 | ## Programmatic usage 70 | 71 | ``` js 72 | var server = require('docker-browser-server') 73 | 74 | var s = server('mafintosh/dev') 75 | 76 | s.on('spawn', function(container) { 77 | console.log('spawned new container', container.id) 78 | }) 79 | 80 | s.on('kill', function(container) { 81 | console.log('killed container', container.id) 82 | }) 83 | 84 | s.listen(8080) 85 | ``` 86 | 87 | ## License 88 | 89 | MIT 90 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var minimist = require('minimist') 4 | var docker = require('./') 5 | 6 | var argv = minimist(process.argv, { 7 | alias: {port:'p', docker:'d', help:'h'}, 8 | boolean: ['hostNetworking', 'offline'], 9 | default: {port:process.env.PORT || 8080} 10 | }) 11 | 12 | var image = argv._[2] 13 | 14 | if (argv.help || !image) { 15 | console.log('Usage: docker-browser-server image [options]') 16 | console.log() 17 | console.log(' --port, -p [8080] (port to listen on)') 18 | console.log(' --docker, -d [$DOCKER_HOST] (optional host of the docker daemon)') 19 | console.log(' --persist (allow persistance of /root in the containers)') 20 | console.log(' --dockerport (expose a docker container port to dockerhost)') 21 | console.log('') 22 | return process.exit(argv.help ? 0 : 1) 23 | } 24 | 25 | if (argv.hostNetworking) argv.beforeCreate = function (config) { 26 | config.HostConfig.NetworkMode = 'host' 27 | } 28 | 29 | var server = docker(image, argv) 30 | 31 | server.on('spawn', function(container) { 32 | console.log('Spawning new container (%s)', container.id) 33 | }) 34 | 35 | server.on('kill', function(container) { 36 | console.log('Killing container (%s)', container.id) 37 | }) 38 | 39 | server.on('listening', function() { 40 | console.log('Server is listening on port %d', server.address().port) 41 | }) 42 | 43 | server.listen(argv.port) 44 | 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var WebSocketServer = require('ws').Server 4 | var freeport = require('freeport') 5 | var request = require('request') 6 | var websocket = require('websocket-stream') 7 | var docker = require('docker-browser-console') 8 | var root = require('root') 9 | var url = require('url') 10 | var send = require('send') 11 | var path = require('path') 12 | var pump = require('pump') 13 | var cors = require('cors') 14 | var net = require('net') 15 | var debug = require('debug')('docker-browser-server') 16 | var debugStream = require('debug-stream')('docker-browser-server-stream') 17 | 18 | module.exports = function(image, opts) { 19 | if (!opts) opts = {} 20 | 21 | var DOCKER_HOST = opts.docker || (process.env.DOCKER_HOST || '127.0.0.1').replace(/^.+:\/\//, '').replace(/:\d+$/, '').replace(/^\/.+$/, '127.0.0.1') 22 | var TMPDIR = process.env.TMPDIR || '/tmp' 23 | 24 | var server = root() 25 | var wss = new WebSocketServer({server: server}) 26 | var containers = {} 27 | 28 | wss.on('connection', function (connection, req) { 29 | var url = req.url.slice(1) 30 | var persist = opts.persist && !!url 31 | var id = url || Math.random().toString(36).slice(2) 32 | var stream = websocket(connection) 33 | debug('socket start', id, +new Date()) 34 | 35 | var startProxy = function(httpPort, cb) { 36 | if (!opts.offline) return cb(null, id+'.c.'+req.headers.host) 37 | 38 | var proxy = net.createServer(function(socket) { 39 | pump(socket, net.connect(httpPort, DOCKER_HOST), socket) 40 | }) 41 | 42 | proxy.once('error', cb) 43 | proxy.listen(0, function() { 44 | var port = proxy.address().port 45 | var subdomain = req.headers.host.split(':')[0]+':'+port 46 | debug('started proxy', subdomain) 47 | cb(null, subdomain, proxy) 48 | }) 49 | } 50 | 51 | freeport(function(err, filesPort) { 52 | if (err) return connection.destroy() 53 | freeport(function(err, dockerHostPort) { 54 | if (err) return connection.destroy() 55 | freeport(function(err, httpPort) { 56 | if (err) return connection.destroy() 57 | startProxy(httpPort, function(err, subdomain, proxy) { 58 | if (err) return connection.destroy() 59 | 60 | var container = containers[id] = { 61 | id: id, 62 | host: 'http://' + subdomain, 63 | ports: {http: httpPort, fs: filesPort, docker: dockerHostPort} 64 | } 65 | 66 | server.emit('spawn', container) 67 | 68 | var ports = {} 69 | var dockercontainerport = opts.dockerport || 9000 70 | ports[httpPort] = 80 71 | ports[filesPort] = 8441 72 | ports[dockerHostPort] = dockercontainerport 73 | 74 | var envs = {} 75 | envs['CONTAINER_ID'] = container.id 76 | envs['HOST'] = container.host 77 | envs['PORT_80'] = httpPort 78 | envs['PORT_8441'] = filesPort 79 | envs['PORT_' + dockercontainerport] = dockerHostPort 80 | if (opts.envs) { 81 | Object.keys(opts.envs).forEach(function(name) { 82 | envs[name] = opts.envs[name] 83 | }) 84 | } 85 | 86 | var dopts = { 87 | tty: opts.tty === undefined ? true : opts.tty, 88 | env: envs, 89 | ports: ports, 90 | volumes: opts.volumes || {}, 91 | beforeCreate: opts.beforeCreate 92 | } 93 | 94 | if (persist) dopts.volumes[TMPDIR + '/'+id] = '/root' 95 | if (opts.trusted) dopts.volumes['/var/run/docker.sock'] = '/var/run/docker.sock' 96 | 97 | stream.on('close', function () { 98 | debug('socket close', id, +new Date()) 99 | }) 100 | 101 | pump(stream, docker(image, dopts), debugStream(), stream, function(err) { 102 | if (proxy) proxy.close() 103 | server.emit('kill', container) 104 | delete containers[id] 105 | }) 106 | }) 107 | }) 108 | }) 109 | }) 110 | }) 111 | 112 | server.all(cors()) 113 | 114 | server.all(function(req, res, next) { 115 | var host = req.headers.host || '' 116 | var i = host.indexOf('.c.') 117 | 118 | if (i > -1) { 119 | var id = host.slice(0, i) 120 | var container = containers.hasOwnProperty(id) && containers[id] 121 | if (container) return pump(req, request('http://'+DOCKER_HOST+':'+container.ports.http+req.url), res) 122 | return res.error(404, 'Could not find container') 123 | } 124 | 125 | next() 126 | }) 127 | 128 | server.get('/-/*', function(req, res) { 129 | send(req, req.params.glob, {root:path.join(__dirname, 'web')}).pipe(res) 130 | }) 131 | 132 | server.get('/containers/{id}', function(req, res) { 133 | var id = req.params.id 134 | var container = containers.hasOwnProperty(id) && containers[id] 135 | if (!container) return res.error(404, 'Could not find container') 136 | res.send(container) 137 | }) 138 | 139 | server.all('/http/{id}/*', function(req, res) { 140 | var id = req.params.id 141 | var url = req.url.slice(('/http/'+id).length) 142 | var container = containers.hasOwnProperty(id) && containers[id] 143 | if (!container) return res.error(404, 'Could not find container') 144 | pump(req, request('http://'+DOCKER_HOST+':'+container.ports.http+url), res) 145 | }) 146 | 147 | server.all('/files/{id}/*', function(req, res) { 148 | var id = req.params.id 149 | var url = req.url.slice(('/files/'+id).length) 150 | var container = containers.hasOwnProperty(id) && containers[id] 151 | if (!container) return res.error(404, 'Could not find container') 152 | pump(req, request('http://'+DOCKER_HOST+':'+container.ports.fs+url), res) 153 | }) 154 | 155 | server.all(function(req, res, next) { 156 | if (!opts.offline) return next() 157 | var id = req.connection.address().address 158 | var container = containers.hasOwnProperty(id) && containers[id] 159 | if (container) return pump(req, request('http://'+DOCKER_HOST+':'+container.ports.http+req.url), res) 160 | next() 161 | }) 162 | 163 | server.get('/bundle.js', '/-/bundle.js') 164 | server.get('/index.html', '/-/index.html') 165 | server.get('/', '/-/index.html') 166 | 167 | return server 168 | } 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-browser-server", 3 | "version": "3.1.4", 4 | "description": "Spawn and expose docker containers over http and websockets", 5 | "main": "index.js", 6 | "dependencies": { 7 | "cors": "^2.4.2", 8 | "debug": "^3.1.0", 9 | "debug-stream": "^3.0.1", 10 | "docker-browser-console": "^6.0.0", 11 | "freeport": "^1.0.3", 12 | "minimist": "^1.1.0", 13 | "pump": "^1.0.0", 14 | "request": "^2.45.0", 15 | "root": "^2.0.0", 16 | "send": "^0.10.1", 17 | "websocket-stream": "^5.0.1", 18 | "ws": "^3.1.0" 19 | }, 20 | "devDependencies": {}, 21 | "bin": { 22 | "docker-browser-server": "./bin.js" 23 | }, 24 | "scripts": { 25 | "start": "node bin.js" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/mafintosh/docker-browser-server.git" 30 | }, 31 | "author": "Mathias Buus (@mafintosh)", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/mafintosh/docker-browser-server/issues" 35 | }, 36 | "homepage": "https://github.com/mafintosh/docker-browser-server" 37 | } 38 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |