├── .gitignore ├── Makefile ├── README.md ├── docker └── Dockerfile ├── lib ├── webexample.ex └── webexample │ ├── config │ ├── config.ex │ ├── dev.ex │ ├── prod.ex │ └── test.ex │ ├── controllers │ └── pages.ex │ ├── router.ex │ └── supervisor.ex ├── mix.exs ├── mix.lock ├── priv └── static │ ├── css │ └── .gitkeep │ └── js │ └── phoenix.js └── test ├── test_helper.exs └── webexample_test.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | docker/webexample 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docker clean 2 | 3 | all: docker 4 | 5 | docker: docker/webexample 6 | cd $< && git pull --rebase && mix deps.get 7 | docker build -t webexample docker/ 8 | 9 | docker/webexample: 10 | cd docker && git clone .. webexample 11 | 12 | 13 | clean: 14 | rm -rf docker/webexample 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Webexample 2 | ========== 3 | 4 | This is an example of packaging and running a Phoenix application with docker. 5 | 6 | Quick start: 7 | 8 | $ make 9 | $ docker run -p 8001:8001 webexample 10 | 11 | 12 | ## Overview 13 | 14 | The build process works as follows: 15 | 16 | * clone this repo to `docker/webexample` 17 | * build a docker image named `webexample` using `docker/` as the context 18 | 19 | **Caveat**: there is currently one unresolved problem with getting mix 20 | dependecies to build as part of the image build process. If you are getting 21 | build errors, run `MIX_ENV=prod mix deps.compile` once inside 22 | `docker/webexample` on your _host machine_. 23 | 24 | If you are running docker inside a VM, you may need to perform a few additional 25 | steps manually. One of them is adding a port forwarding rule in the VM settings 26 | to map a port from the VM to the host OS. 27 | 28 | Note that it is OK to hardcode the port number used inside the image the 29 | way we did. When you run the image, you can map the container port to any other 30 | available port on the host (or the VM in the case when docker itself is running 31 | in a VM). 32 | 33 | 34 | ## Points of interest 35 | 36 | ### Elixir dependencies 37 | 38 | The current setup fetches dependencies before building the docker image to 39 | speed up the rebuild process. Depending on how you are planning to use docker, 40 | you might want to include that as part of the image build or, alternatively, 41 | add a rule to the Makefile that will run `mix deps.update --all` after pulling 42 | the latest changes with git. 43 | 44 | ### Iterative development 45 | 46 | An important thing to understand about this example: it demoes how one would 47 | package an Elixir app in a docker image for deployment. This works fine if you 48 | don't need to rebuild the image too often. This workflow differs in a number of 49 | ways from using docker as part of the development process. 50 | 51 | For the latter case you will have to come up with ways to make docker 52 | automatically pick up the latest version of your project's code and update any 53 | other environment settings in the presences of code changes. You could mount 54 | the directory with your project code as a host volume in the running docker 55 | container (passing the `-v` option to `docker run`). That implies building the 56 | project either when the container starts or beforehand. 57 | 58 | ### Alternative build strategies 59 | 60 | With docker it is possible to create "builder images". Those are images that 61 | produce build artifacts when run. You could create such an image for your 62 | project to have the compilation and running phases separate but still using the 63 | same environment and Elixir versions (enforced via a single base image). This 64 | workflow is not explored in the present demo. 65 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alco/ubuntu-elixir:v0.13.3 2 | MAINTAINER Alexei Sholik 3 | 4 | ## Prerequisites ## 5 | 6 | # Install anything else you want to have as part of your container. 7 | # In this case we need git for Mix to pick up git-deps from mix.lock. 8 | RUN sudo apt-get update 9 | RUN sudo apt-get install -y git 10 | 11 | RUN mix local.rebar 12 | RUN mix local.hex --force 13 | RUN mix hex.update 14 | 15 | 16 | ## Install the app ## 17 | 18 | # We assume the deps have already been fetched 19 | ADD webexample /deploy/webexample 20 | 21 | 22 | ## Compile ## 23 | 24 | # It is important to set MIX_ENV *before* compiling the code 25 | ENV MIX_ENV prod 26 | 27 | # Setting working directory to the app's root should also happen before 28 | # compilation 29 | WORKDIR /deploy/webexample 30 | 31 | # This will run during build process, so that we don't need to compile 32 | # anything when running the container 33 | RUN mix compile 34 | 35 | 36 | ## Set up running environment ## 37 | 38 | # If you're running docker in a VM (for instance, if you're on OS X), you'll 39 | # have to add a port forwarding rule for this port in the VM settings to make 40 | # it available on the host OS 41 | ENV PORT 8001 42 | 43 | # Could have set ENTRYPOINT here, but I couldn't not override it from command 44 | # line. Setting CMD allows us to run the container with e.g. bash in the 45 | # future if needed 46 | 47 | # We don't use 'mix phoenix.start' here because we need to pass additional 48 | # options to the run task 49 | CMD ["mix", "run", "-e", "Webexample.Router.start", \ 50 | "--no-deps-check", "--no-compile", "--no-halt"] 51 | -------------------------------------------------------------------------------- /lib/webexample.ex: -------------------------------------------------------------------------------- 1 | defmodule Webexample do 2 | use Application.Behaviour 3 | 4 | # See http://elixir-lang.org/docs/stable/Application.Behaviour.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | Webexample.Supervisor.start_link 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/webexample/config/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Webexample.Config do 2 | use Phoenix.Config.App 3 | 4 | config :router, port: System.get_env("PORT") 5 | 6 | config :plugs, code_reload: false 7 | 8 | config :logger, level: :error 9 | end 10 | 11 | 12 | -------------------------------------------------------------------------------- /lib/webexample/config/dev.ex: -------------------------------------------------------------------------------- 1 | defmodule Webexample.Config.Dev do 2 | use Webexample.Config 3 | 4 | config :router, port: 4000, 5 | ssl: false, 6 | # Full error reports are enabled 7 | consider_all_requests_local: true 8 | 9 | config :plugs, code_reload: true 10 | 11 | config :logger, level: :debug 12 | end 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/webexample/config/prod.ex: -------------------------------------------------------------------------------- 1 | defmodule Webexample.Config.Prod do 2 | use Webexample.Config 3 | 4 | config :router, port: System.get_env("PORT"), 5 | # Full error reports are disabled 6 | consider_all_requests_local: false 7 | 8 | config :plugs, code_reload: false 9 | 10 | config :logger, level: :error 11 | end 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/webexample/config/test.ex: -------------------------------------------------------------------------------- 1 | defmodule Webexample.Config.Test do 2 | use Webexample.Config 3 | 4 | config :router, port: 4001, 5 | ssl: false, 6 | # Full error reports are enabled 7 | consider_all_requests_local: true 8 | 9 | config :plugs, code_reload: true 10 | 11 | config :logger, level: :debug 12 | end 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/webexample/controllers/pages.ex: -------------------------------------------------------------------------------- 1 | defmodule Webexample.Controllers.Pages do 2 | use Phoenix.Controller 3 | 4 | def index(conn) do 5 | text conn, "Hello world" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/webexample/router.ex: -------------------------------------------------------------------------------- 1 | defmodule Webexample.Router do 2 | use Phoenix.Router 3 | 4 | plug Plug.Static, at: "/static", from: :webexample 5 | get "/", Webexample.Controllers.Pages, :index, as: :page 6 | end 7 | -------------------------------------------------------------------------------- /lib/webexample/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Webexample.Supervisor do 2 | use Supervisor.Behaviour 3 | 4 | def start_link do 5 | :supervisor.start_link(__MODULE__, []) 6 | end 7 | 8 | def init([]) do 9 | children = [] 10 | 11 | # See http://elixir-lang.org/docs/stable/Supervisor.Behaviour.html 12 | # for other strategies and supported options 13 | supervise(children, strategy: :one_for_one) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Webexample.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ app: :webexample, 6 | version: "0.0.1", 7 | elixir: "~> 0.13.2", 8 | deps: deps ] 9 | end 10 | 11 | # Configuration for the OTP application 12 | def application do 13 | [ 14 | mod: { Webexample, [] }, 15 | applications: [:phoenix] 16 | ] 17 | end 18 | 19 | # Returns the list of dependencies in the format: 20 | # { :foobar, git: "https://github.com/elixir-lang/foobar.git", tag: "0.1" } 21 | # 22 | # To specify particular versions, regardless of the tag, do: 23 | # { :barbat, "~> 0.1", github: "elixir-lang/barbat" } 24 | defp deps do 25 | [ 26 | {:phoenix, "0.2.4"}, 27 | {:jazz, github: "meh/jazz", ref: "7af3b74e58eb1a3fc6b9874a2077efa420f6dfcc"}, 28 | {:cowboy, github: "extend/cowboy", override: true, ref: "05024529679d1d0203b8dcd6e2932cc2a526d370"}, 29 | ] 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"cowboy": {:git, "git://github.com/extend/cowboy.git", "05024529679d1d0203b8dcd6e2932cc2a526d370", [ref: "05024529679d1d0203b8dcd6e2932cc2a526d370"]}, 2 | "cowlib": {:git, "git://github.com/extend/cowlib.git", "f58340a0044856bb508df03cfe94cf79308380a2", [ref: "0.6.1"]}, 3 | "ex_conf": {:package, "0.1.1"}, 4 | "inflex": {:package, "0.2.0"}, 5 | "jazz": {:git, "git://github.com/meh/jazz.git", "7af3b74e58eb1a3fc6b9874a2077efa420f6dfcc", [ref: "7af3b74e58eb1a3fc6b9874a2077efa420f6dfcc"]}, 6 | "phoenix": {:package, "0.2.4"}, 7 | "plug": {:package, "0.4.3"}, 8 | "ranch": {:git, "git://github.com/extend/ranch.git", "5df1f222f94e08abdcab7084f5e13027143cc222", [ref: "0.9.0"]}} 9 | -------------------------------------------------------------------------------- /priv/static/css/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alco/phoenix-docker-example/00bfbd0212e8079e1ae7d52dafc6fbe798ff8dfd/priv/static/css/.gitkeep -------------------------------------------------------------------------------- /priv/static/js/phoenix.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.7.1 2 | (function() { 3 | this.Phoenix = {}; 4 | 5 | this.Phoenix.Channel = (function() { 6 | Channel.prototype.bindings = null; 7 | 8 | function Channel(channel, topic, message, callback, socket) { 9 | this.channel = channel; 10 | this.topic = topic; 11 | this.message = message; 12 | this.callback = callback; 13 | this.socket = socket; 14 | this.reset(); 15 | } 16 | 17 | Channel.prototype.reset = function() { 18 | return this.bindings = []; 19 | }; 20 | 21 | Channel.prototype.on = function(event, callback) { 22 | return this.bindings.push({ 23 | event: event, 24 | callback: callback 25 | }); 26 | }; 27 | 28 | Channel.prototype.isMember = function(channel, topic) { 29 | return this.channel === channel && this.topic === topic; 30 | }; 31 | 32 | Channel.prototype.off = function(event) { 33 | var bind; 34 | return this.bindings = (function() { 35 | var _i, _len, _ref, _results; 36 | _ref = this.bindings; 37 | _results = []; 38 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 39 | bind = _ref[_i]; 40 | if (bind.event !== event) { 41 | _results.push(bind); 42 | } 43 | } 44 | return _results; 45 | }).call(this); 46 | }; 47 | 48 | Channel.prototype.trigger = function(triggerEvent, msg) { 49 | var callback, event, _i, _len, _ref, _ref1, _results; 50 | _ref = this.bindings; 51 | _results = []; 52 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 53 | _ref1 = _ref[_i], event = _ref1.event, callback = _ref1.callback; 54 | if (event === triggerEvent) { 55 | _results.push(callback(msg)); 56 | } 57 | } 58 | return _results; 59 | }; 60 | 61 | Channel.prototype.send = function(event, message) { 62 | return this.socket.send({ 63 | channel: this.channel, 64 | topic: this.topic, 65 | event: event, 66 | message: message 67 | }); 68 | }; 69 | 70 | Channel.prototype.leave = function(message) { 71 | if (message == null) { 72 | message = {}; 73 | } 74 | this.socket.leave(this.channel, this.topic, message); 75 | return this.reset(); 76 | }; 77 | 78 | return Channel; 79 | 80 | })(); 81 | 82 | this.Phoenix.Socket = (function() { 83 | Socket.prototype.conn = null; 84 | 85 | Socket.prototype.endPoint = null; 86 | 87 | Socket.prototype.channels = null; 88 | 89 | Socket.prototype.sendBuffer = null; 90 | 91 | Socket.prototype.sendBufferTimer = null; 92 | 93 | Socket.prototype.flushEveryMs = 50; 94 | 95 | Socket.prototype.reconnectTimer = null; 96 | 97 | Socket.prototype.reconnectAfterMs = 5000; 98 | 99 | function Socket(endPoint) { 100 | this.endPoint = endPoint; 101 | this.channels = []; 102 | this.sendBuffer = []; 103 | this.resetBufferTimer(); 104 | this.reconnect(); 105 | } 106 | 107 | Socket.prototype.close = function(callback) { 108 | if (this.conn != null) { 109 | this.conn.onclose = (function(_this) { 110 | return function() {}; 111 | })(this); 112 | this.conn.close(); 113 | this.conn = null; 114 | } 115 | return typeof callback === "function" ? callback() : void 0; 116 | }; 117 | 118 | Socket.prototype.reconnect = function() { 119 | return this.close((function(_this) { 120 | return function() { 121 | _this.conn = new WebSocket(_this.endPoint); 122 | _this.conn.onopen = function() { 123 | return _this.onOpen(); 124 | }; 125 | _this.conn.onerror = function(error) { 126 | return _this.onError(error); 127 | }; 128 | _this.conn.onmessage = function(event) { 129 | return _this.onMessage(event); 130 | }; 131 | return _this.conn.onclose = function(event) { 132 | return _this.onClose(event); 133 | }; 134 | }; 135 | })(this)); 136 | }; 137 | 138 | Socket.prototype.resetBufferTimer = function() { 139 | clearTimeout(this.sendBufferTimer); 140 | return this.sendBufferTimer = setTimeout(((function(_this) { 141 | return function() { 142 | return _this.flushSendBuffer(); 143 | }; 144 | })(this)), this.flushEveryMs); 145 | }; 146 | 147 | Socket.prototype.onOpen = function() { 148 | clearInterval(this.reconnectTimer); 149 | return this.rejoinAll(); 150 | }; 151 | 152 | Socket.prototype.onClose = function(event) { 153 | if (typeof console.log === "function") { 154 | console.log("WS close: " + event); 155 | } 156 | clearInterval(this.reconnectTimer); 157 | return this.reconnectTimer = setInterval(((function(_this) { 158 | return function() { 159 | return _this.reconnect(); 160 | }; 161 | })(this)), this.reconnectAfterMs); 162 | }; 163 | 164 | Socket.prototype.onError = function(error) { 165 | return typeof console.log === "function" ? console.log("WS error: " + error) : void 0; 166 | }; 167 | 168 | Socket.prototype.connectionState = function() { 169 | var _ref, _ref1; 170 | switch ((_ref = (_ref1 = this.conn) != null ? _ref1.readyState : void 0) != null ? _ref : 3) { 171 | case 0: 172 | return "connecting"; 173 | case 1: 174 | return "open"; 175 | case 2: 176 | return "closing"; 177 | case 3: 178 | return "closed"; 179 | } 180 | }; 181 | 182 | Socket.prototype.isConnected = function() { 183 | return this.connectionState() === "open"; 184 | }; 185 | 186 | Socket.prototype.rejoinAll = function() { 187 | var chan, _i, _len, _ref, _results; 188 | _ref = this.channels; 189 | _results = []; 190 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 191 | chan = _ref[_i]; 192 | _results.push(this.rejoin(chan)); 193 | } 194 | return _results; 195 | }; 196 | 197 | Socket.prototype.rejoin = function(chan) { 198 | var channel, message, topic; 199 | chan.reset(); 200 | channel = chan.channel, topic = chan.topic, message = chan.message; 201 | this.send({ 202 | channel: channel, 203 | topic: topic, 204 | event: "join", 205 | message: message 206 | }); 207 | return chan.callback(chan); 208 | }; 209 | 210 | Socket.prototype.join = function(channel, topic, message, callback) { 211 | var chan; 212 | chan = new Phoenix.Channel(channel, topic, message, callback, this); 213 | this.channels.push(chan); 214 | if (this.isConnected()) { 215 | return this.rejoin(chan); 216 | } 217 | }; 218 | 219 | Socket.prototype.leave = function(channel, topic, message) { 220 | var c; 221 | if (message == null) { 222 | message = {}; 223 | } 224 | this.send({ 225 | channel: channel, 226 | topic: topic, 227 | event: "leave", 228 | message: message 229 | }); 230 | return this.channels = (function() { 231 | var _i, _len, _ref, _results; 232 | _ref = this.channels; 233 | _results = []; 234 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 235 | c = _ref[_i]; 236 | if (!(c.isMember(channel, topic))) { 237 | _results.push(c); 238 | } 239 | } 240 | return _results; 241 | }).call(this); 242 | }; 243 | 244 | Socket.prototype.send = function(data) { 245 | var callback; 246 | callback = (function(_this) { 247 | return function() { 248 | return _this.conn.send(JSON.stringify(data)); 249 | }; 250 | })(this); 251 | if (this.isConnected()) { 252 | return callback(); 253 | } else { 254 | return this.sendBuffer.push(callback); 255 | } 256 | }; 257 | 258 | Socket.prototype.flushSendBuffer = function() { 259 | var callback, _i, _len, _ref; 260 | if (this.isConnected() && this.sendBuffer.length > 0) { 261 | _ref = this.sendBuffer; 262 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 263 | callback = _ref[_i]; 264 | callback(); 265 | } 266 | this.sendBuffer = []; 267 | } 268 | return this.resetBufferTimer(); 269 | }; 270 | 271 | Socket.prototype.onMessage = function(rawMessage) { 272 | var chan, channel, event, message, topic, _i, _len, _ref, _ref1, _results; 273 | if (typeof console.log === "function") { 274 | console.log(rawMessage); 275 | } 276 | _ref = JSON.parse(rawMessage.data), channel = _ref.channel, topic = _ref.topic, event = _ref.event, message = _ref.message; 277 | _ref1 = this.channels; 278 | _results = []; 279 | for (_i = 0, _len = _ref1.length; _i < _len; _i++) { 280 | chan = _ref1[_i]; 281 | if (chan.isMember(channel, topic)) { 282 | _results.push(chan.trigger(event, message)); 283 | } 284 | } 285 | return _results; 286 | }; 287 | 288 | return Socket; 289 | 290 | })(); 291 | 292 | }).call(this); 293 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | -------------------------------------------------------------------------------- /test/webexample_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WebexampleTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert(true) 6 | end 7 | end 8 | --------------------------------------------------------------------------------