Setup Docker image with releases (#244)

* Setup Docker image with releases

* Support ip env variable and use it for Docker deployment

* Autofocus auth forms

* Rename ip env var

* Update option lists

* Make distribution cookie configurable

* Update README.md

Co-authored-by: José Valim <jose.valim@dashbit.co>

* Include git in the final image

* Remove unnecessary build dependency

* Improve file permissions handling and add more comments

* Use namespaced home directory

* Update README with all running options

* Update base image

* Reference official Docker image in the README

Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
Jonatan Kłosko 2021-04-27 16:34:02 +02:00 committed by GitHub
parent cc2820f17e
commit ac8e1e30f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 234 additions and 21 deletions

15
.dockerignore Normal file
View file

@ -0,0 +1,15 @@
# Mirrors .gitignore
/_build/
/cover/
/deps/
/doc/
/.fetch
erl_crash.dump
*.ez
livebook-*.tar
npm-debug.log
/assets/node_modules/
/priv/static_dev/
/tmp/
/livebook

66
Dockerfile Normal file
View file

@ -0,0 +1,66 @@
# Stage 1
# Builds the Livebook release
FROM hexpm/elixir:1.12.0-rc.1-erlang-24.0-rc3-alpine-3.13.3 AS build
RUN apk add --no-cache build-base git
WORKDIR /app
# Install hex and rebar
RUN mix local.hex --force && \
mix local.rebar --force
# Build for production
ENV MIX_ENV=prod
# Install mix dependencies
COPY mix.exs mix.lock ./
COPY config config
RUN mix do deps.get, deps.compile
# Compile and build the release
COPY priv priv
COPY lib lib
# We need README.md during compilation
# (look for @external_resource "README.md")
COPY README.md README.md
RUN mix do compile, release
# Stage 2
# Prepares the runtime environment and copies over the relase.
# We use the same base image, because we need Erlang, Elixir and Mix
# during runtime to spawn the Livebook standalone runtimes.
# Consequently the release doesn't include ERTS as we have it anyway.
FROM hexpm/elixir:1.12.0-rc.1-erlang-24.0-rc3-alpine-3.13.3
RUN apk add --no-cache \
# Runtime dependencies
openssl ncurses-libs \
# In case someone uses `Mix.install/2` and point to a git repo
git
# Run in the /data directory by default, makes for
# a good place for the user to mount local volume
WORKDIR /data
ENV HOME=/home/livebook
# Make sure someone running the container with `--user`
# has permissions to the home dir (for `Mix.install/2` cache)
RUN mkdir $HOME && chmod 777 $HOME
# Install hex and rebar for `Mix.install/2` and Mix runtime
RUN mix local.hex --force && \
mix local.rebar --force
# Override the default 127.0.0.1 address, so that the app
# can be accessed outside the container by binding ports
ENV LIVEBOOK_IP 0.0.0.0
# Copy the release build from the previous stage
COPY --from=build /app/_build/prod/rel/livebook /app
# Make release executables available to any user,
# in case someone runs the container with `--user`
RUN find /app -executable -type f -exec chmod +x {} +
CMD [ "/app/bin/livebook", "start" ]

View file

@ -3,7 +3,7 @@
Livebook is a web application for writing interactive and collaborative code notebooks. It features:
* A deployable web app built with [Phoenix LiveView](https://github.com/phoenixframework/phoenix_live_view) where users can create, fork, and run multiple notebooks.
* Each notebook is made of multiple sections: each section is made of Markdown and Elixir cells. Code in Elixir cells can be evaluated on demand. Mathematical formulas are also supported via [KaTeX](https://katex.org/).
* Persistence: notebooks can be persisted to disk through the `.livemd` format, which is a subset of Markdown. This means your notebooks can be saved for later, easily shared, and they also play well with version control.
@ -24,16 +24,63 @@ The current version provides only the initial step of our Livebook vision. Our p
## Usage
For now, the best way to run Livebook is by cloning it and running it locally:
We provide several distinct methods of running Livebook,
pick the one that best fits your use case.
$ git clone https://github.com/elixir-nx/livebook.git
$ cd livebook
$ mix deps.get --only prod
$ MIX_ENV=prod mix phx.server
### Mix
You can run latest Livebook directly with Mix.
```shell
git clone https://github.com/elixir-nx/livebook.git
cd livebook
mix deps.get --only prod
# Run the Livebook server
MIX_ENV=prod mix phx.server
```
You will need [Elixir v1.11](https://elixir-lang.org/install.html) or later.
We will work on other distribution modes (escripts, Docker images, etc) once we start distributing official releases.
### Escript
Running Livebook using Escript makes for a very convenient option
for local usage and provides easy configuration via CLI options.
```shell
# Currently you need to build the Escript manually,
# we will publish it to Hex once we release the first version
git clone https://github.com/elixir-nx/livebook.git
cd livebook
mix deps.get --only prod
MIX_ENV=prod mix escript.build
# Start the Livebook server
./livebook server
# See all the configuration options
./livebook server --help
```
### Docker
Running Livebook using Docker is a great option for cloud deployments
and also for local usage in case you don't have Elixir installed.
```shell
# Running with the default configuration
docker run -p 8080:8080 livebook/livebook
# In order to access and save notebooks directly to your machine
# you can mount a local directory into the container.
# Make sure to specify the user with "-u $(id -u):$(id -g)"
# so that the created files have proper permissions
docker run -p 8080:8080 -u $(id -u):$(id -g) -v <LOCAL_DIR>:/data livebook/livebook
# You can configure Livebook using environment variables,
# for all options see the dedicated "Environment variables" section below
docker run -p 8080:8080 -e LIVEBOOK_PASSWORD="securesecret" livebook/livebook
```
### Security considerations
@ -46,13 +93,18 @@ For this reason, Livebook only binds to the 127.0.0.1, allowing access to happen
The following environment variables configure Livebook:
* LIVEBOOK_COOKIE - sets the cookie for running Livebook in a cluster.
Defaults to a random string that is generated on boot.
* LIVEBOOK_IP - sets the ip address to start the web application on. Must be a valid IPv4 or IPv6 address.
* LIVEBOOK_PASSWORD - sets a password that must be used to access Livebook. Must be at least 12 characters. Defaults to token authentication.
* LIVEBOOK_PORT - sets the port Livebook runs on. If you want multiple instances to run on the same domain but different ports, you also need to set `LIVEBOOK_SECRET_KEY_BASE`. Defaults to 8080.
* LIVEBOOK_PORT - sets the port Livebook runs on. If you want multiple instances to run on the same domain but different ports, you also need to set 'LIVEBOOK_SECRET_KEY_BASE'. Defaults to 8080.
* LIVEBOOK_SECRET_KEY_BASE - sets a secret key that is used to sign and encrypt the session and other payloads used by Livebook. Must be at least 64 characters long and it can be generated by commands such as: `openssl rand -base64 48`. Defaults to a random secret on every boot.
* LIVEBOOK_ROOT_PATH - sets the root path to use for file selection.
* LIVEBOOK_ROOT_PATH - the root path to use for file selection.
* LIVEBOOK_SECRET_KEY_BASE - sets a secret key that is used to sign and encrypt the session and other payloads used by Livebook. Must be at least 64 characters long and it can be generated by commands such as: 'openssl rand -base64 48'. Defaults to a random secret on every boot.
<!-- Environment variables -->
## License

View file

@ -1,7 +1,9 @@
import Config
# Default bind and port for production
config :livebook, LivebookWeb.Endpoint, http: [ip: {127, 0, 0, 1}, port: 8080]
config :livebook, LivebookWeb.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 8080],
server: true
# Start log-level in notice by default to reduce output
config :logger, level: :notice

View file

@ -17,3 +17,11 @@ end
if port = Livebook.Config.port!("LIVEBOOK_PORT") do
config :livebook, LivebookWeb.Endpoint, http: [port: port]
end
if ip = Livebook.Config.ip!("LIVEBOOK_IP") do
config :livebook, LivebookWeb.Endpoint, http: [ip: ip]
end
config :livebook,
:cookie,
Livebook.Config.cookie!("LIVEBOOK_COOKIE") || Livebook.Utils.random_cookie()

View file

@ -7,6 +7,7 @@ defmodule Livebook.Application do
def start(_type, _args) do
ensure_distribution!()
set_cookie()
children = [
# Start the Telemetry supervisor
@ -69,6 +70,11 @@ defmodule Livebook.Application do
end
end
defp set_cookie() do
cookie = Application.fetch_env!(:livebook, :cookie)
Node.set_cookie(cookie)
end
defp get_node_type_and_name() do
Application.get_env(:livebook, :node) || {:shortnames, random_short_name()}
end

View file

@ -84,6 +84,37 @@ defmodule Livebook.Config do
end
end
@doc """
Parses and validates the ip from env.
"""
def ip!(env) do
if ip = System.get_env(env) do
ip!(env, ip)
end
end
@doc """
Parses and validates the ip within context.
"""
def ip!(context, ip) do
case ip |> String.to_charlist() |> :inet.parse_address() do
{:ok, ip} ->
ip
{:error, :einval} ->
abort!("expected #{context} to be a valid ipv4 or ipv6 address, got: #{ip}")
end
end
@doc """
Parses the cookie from env.
"""
def cookie!(env) do
if cookie = System.get_env(env) do
String.to_atom(cookie)
end
end
@doc """
Parses and validates the password from env.
"""

View file

@ -19,6 +19,14 @@ defmodule Livebook.Utils do
:crypto.strong_rand_bytes(5) |> Base.encode32(case: :lower)
end
@doc """
Generates a random cookie for a distributed node.
"""
@spec random_cookie() :: atom()
def random_cookie() do
:crypto.strong_rand_bytes(42) |> Base.url_encode64() |> String.to_atom()
end
@doc """
Converts the given name to node identifier.
"""

View file

@ -2,6 +2,7 @@ defmodule LivebookCLI.Server do
@moduledoc false
@behaviour LivebookCLI.Task
@external_resource "README.md"
[_, environment_variables, _] =
@ -9,7 +10,6 @@ defmodule LivebookCLI.Server do
|> File.read!()
|> String.split("<!-- Environment variables -->")
@external_resource "README.md"
@environment_variables String.trim(environment_variables)
@impl true
@ -19,12 +19,15 @@ defmodule LivebookCLI.Server do
Available options:
--cookie Sets a cookie for the app distributed node
--ip The ip address to start the web application on, defaults to 127.0.0.1
Must be a valid IPv4 or IPv6 address
--name Set a name for the app distributed node
--no-token Disable token authentication, enabled by default
If LIVEBOOK_PASSWORD is set, it takes precedence over token auth
--sname Set a short name for the app distributed node
-p, --port The port to start the web application on, defaults to 8080
--root-path The root path to use for file selection
--sname Set a short name for the app distributed node
The --help option can be given to print this notice.
@ -62,8 +65,8 @@ defmodule LivebookCLI.Server do
end
defp start_server() do
Application.put_env(:phoenix, :serve_endpoints, true, persistent: true)
# We configure the endpoint with `server: true`,
# so it's gonna start listening
case Application.ensure_all_started(:livebook) do
{:ok, _} -> :ok
{:error, _} -> :error
@ -71,11 +74,13 @@ defmodule LivebookCLI.Server do
end
@switches [
token: :boolean,
port: :integer,
cookie: :string,
ip: :string,
name: :string,
port: :integer,
root_path: :string,
sname: :string,
root_path: :string
token: :boolean
]
@aliases [
@ -108,6 +113,11 @@ defmodule LivebookCLI.Server do
opts_to_config(opts, [{:livebook, LivebookWeb.Endpoint, http: [port: port]} | config])
end
defp opts_to_config([{:ip, ip} | opts], config) do
ip = Livebook.Config.ip!("--ip", ip)
opts_to_config(opts, [{:livebook, LivebookWeb.Endpoint, http: [ip: ip]} | config])
end
defp opts_to_config([{:root_path, root_path} | opts], config) do
root_path = Livebook.Config.root_path!("--root-path", root_path)
opts_to_config(opts, [{:livebook, :root_path, root_path} | config])
@ -123,5 +133,10 @@ defmodule LivebookCLI.Server do
opts_to_config(opts, [{:livebook, :node, {:longnames, name}} | config])
end
defp opts_to_config([{:cookie, cookie} | opts], config) do
cookie = String.to_atom(cookie)
opts_to_config(opts, [{:livebook, :cookie, cookie} | config])
end
defp opts_to_config([_opt | opts], config), do: opts_to_config(opts, config)
end

View file

@ -13,7 +13,7 @@
<div class="text-2xl text-gray-50 w-full pt-2">
<form method="post" class="flex flex-col space-y-4 items-center">
<input type="hidden" value="<%= Phoenix.Controller.get_csrf_token() %>" name="_csrf_token"/>
<input type="password" name="password" class="input" placeholder="Password" />
<input type="password" name="password" class="input" placeholder="Password" autofocus />
<button type="submit" class="button button-blue">
Authenticate
</button>

View file

@ -24,7 +24,7 @@
<div class="text-2xl text-gray-50 w-full pt-2">
<form method="get" class="flex flex-col space-y-4 items-center">
<input type="text" name="token" class="input" placeholder="Token" />
<input type="text" name="token" class="input" placeholder="Token" autofocus />
<button type="submit" class="button button-blue">
Authenticate
</button>

12
mix.exs
View file

@ -11,7 +11,8 @@ defmodule Livebook.MixProject do
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
escript: escript()
escript: escript(),
releases: releases()
]
end
@ -57,4 +58,13 @@ defmodule Livebook.MixProject do
app: nil
]
end
defp releases() do
[
livebook: [
include_executables_for: [:unix],
include_erts: false
]
]
end
end