Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

1. Check related deps for required version bumps and compatibility (`phoenix_ecto`, `phoenix_pubsub_redis`, `phoenix_html`)
2. Bump version in related files below
3. Run tests:
3. Bump external dependency version in related external files below
4. Run tests:
- `mix test` in the root folder
- `mix test` in the `installer/` folder
4. Commit, push code
5. Publish `phx_new` and `phoenix` packages and docs after pruning any extraneous uncommitted files
6. Test installer by generating a new app, running `mix deps.get`, and compiling
7. Publish to `npm` with `npm publish`
8. Start -dev version in related files below
5. Commit, push code
6. Publish `phx_new` and `phoenix` packages and docs after pruning any extraneous uncommitted files
7. Test installer by generating a new app, running `mix deps.get`, and compiling
8. Publish to `npm` with `npm publish`
9. Start -dev version in related files below

## Files with version

Expand All @@ -18,3 +19,7 @@
* `installer/mix.exs`
* `package.json`
* `assets/package.json`

## Files with external dependency versions
* `priv/templates/phx.gen.release/Docker.eex` (debian)
* `priv/templates/phx.gen.release/Docker.eex` (esbuild)
6 changes: 4 additions & 2 deletions installer/lib/phx_new/generator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,14 @@ defmodule Phx.New.Generator do
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
\"""

maybe_ipv6 = if System.get_env("ECTO_IPV6"), do: [:inet6], else: []
""",
prod_config: """
# ssl: true,
# socket_options: [:inet6],
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
socket_options: maybe_ipv6
"""
]
end
Expand Down
4 changes: 1 addition & 3 deletions installer/templates/phx_single/config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import Config
# manifest is generated by the `mix phx.digest` task,
# which you should run after static files are built and
# before starting your production server.
config :<%= @web_app_name %>, <%= @endpoint_module %>,
url: [host: "example.com", port: 80],
cache_static_manifest: "priv/static/cache_manifest.json"
config :<%= @web_app_name %>, <%= @endpoint_module %>, cache_static_manifest: "priv/static/cache_manifest.json"

# Do not print debug messages in production
config :logger, level: :info
Expand Down
8 changes: 7 additions & 1 deletion installer/templates/phx_single/config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,21 @@ if config_env() == :prod do
You can generate one by calling: mix phx.gen.secret
"""

server? = System.get_env("PHX_SERVER") != nil
host = System.get_env("PHX_HOST") || "example.com"
port = String.to_integer(System.get_env("PORT") || "4000")

config :<%= @app_name %>, <%= @endpoint_module %>,
url: [host: host, port: 443],
http: [
# Enable IPv6 and bind on all interfaces.
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
# See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: String.to_integer(System.get_env("PORT") || "4000")
port: port
],
server: server?,
secret_key_base: secret_key_base

# ## Using releases
Expand Down
10 changes: 9 additions & 1 deletion installer/test/phx_new_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,15 @@ defmodule Mix.Tasks.Phx.NewTest do
end
assert_file "phx_blog/config/dev.exs", config
assert_file "phx_blog/config/test.exs", config
assert_file "phx_blog/config/runtime.exs", config
assert_file "phx_blog/config/runtime.exs", fn file ->
assert file =~ config
assert file =~ ~S|ipv6? = System.get_env("ECTO_IPV6") == "true"|
assert file =~ ~S|server? = System.get_env("PHX_SERVER") == "true"|
assert file =~ "host = System.get_env(\"PHX_HOST\") || \"example.com\""
assert file =~ ~S|url: [host: host, port: 80],|
assert file =~ ~S|server: server?|
assert file =~ ~S|socket_options: if(ipv6?, do: [:inet6], else: []),|
end
assert_file "phx_blog/config/test.exs", ~R/database: "phx_blog_test#\{System.get_env\("MIX_TEST_PARTITION"\)\}"/
assert_file "phx_blog/lib/phx_blog/repo.ex", ~r"defmodule PhxBlog.Repo"
assert_file "phx_blog/lib/phx_blog_web.ex", ~r"defmodule PhxBlogWeb"
Expand Down
179 changes: 179 additions & 0 deletions lib/mix/tasks/phx.gen.release.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
defmodule Mix.Tasks.Phx.Gen.Release do
@shortdoc "Generates release files and optional Dockerfile for release-based deployments"

@moduledoc """
Generates release files and optional Dockerfile for release-based deployments.

The following release files are created:

* `lib/app_name/release.exs` - A release module containing tasks for running
migrations inside a release

* `rel/overlays/bin/migrate` - A migrate script for conveniently invoking
the release system migrations

* `rel/overlays/bin/server` - A server script for conveniently invoking
the release system with environment variables to start the phoenix web server

Note, the `rel/overlays` directory is copied into the release build by default when
running `mix release`.

When the `--docker` flag is passed, the following docker files are generated:

* `Dockerfile` - The Dockerfile for use in any standard docker deployment

* `.dockerignore` - A docker ignore file with standard elixir defaults

For extended release configuration, the `mix release.init`task can be used
in addition to this task. See the `Mix.Release` docs for more details.
"""

use Mix.Task

@doc false
def run(args) do
docker? = "--docker" in args
ecto? = "--ecto" in args || Code.ensure_loaded?(Ecto)

if Mix.Project.umbrella?() do
Mix.raise("""
mix phx.gen.release is not supported in umbrella applications.

Run this task in your web application instead.
""")
end

app = Mix.Phoenix.otp_app()
app_namespace = Mix.Phoenix.base()

binding = [
app_namespace: app_namespace,
otp_app: app,
elixir_vsn: System.version(),
otp_vsn: otp_vsn()
]

Mix.Phoenix.copy_from(paths(), "priv/templates/phx.gen.release", binding, [
{:eex, "rel/server.sh.eex", "rel/overlays/bin/server"},
{:eex, "rel/server.bat.eex", "rel/overlays/bin/server.bat"}
])

if ecto? do
Mix.Phoenix.copy_from(paths(), "priv/templates/phx.gen.release", binding, [
{:eex, "rel/migrate.sh.eex", "rel/overlays/bin/migrate"},
{:eex, "rel/migrate.bat.eex", "rel/overlays/bin/migrate.bat"},
{:eex, "release.ex", Mix.Phoenix.context_lib_path(app, "release.ex")}
])
end

if docker? do
Mix.Phoenix.copy_from(paths(), "priv/templates/phx.gen.release", binding, [
{:eex, "Dockerfile.eex", "Dockerfile"},
{:eex, "dockerignore.eex", ".dockerignore"}
])
end

File.chmod!("rel/overlays/bin/server", 0o755)
File.chmod!("rel/overlays/bin/server.bat", 0o755)
if ecto? do
File.chmod!("rel/overlays/bin/migrate", 0o755)
File.chmod!("rel/overlays/bin/migrate.bat", 0o755)
end

Mix.shell().info("""

Your application is ready to be deployed in a release!

# To start your system
_build/dev/rel/#{app}/bin/#{app} start

# To start your system with the Phoenix server running
_build/dev/rel/#{app}/bin/server
#{ecto? && ecto_instructions(app)}
Once the release is running:

# To connect to it remotely
_build/dev/rel/#{app}/bin/#{app} remote

# To stop it gracefully (you may also send SIGINT/SIGTERM)
_build/dev/rel/#{app}/bin/#{app} stop

To list all commands:

_build/dev/rel/#{app}/bin/#{app}
""")

ecto? &&
post_install_instructions("config/runtime.exs", ~r/ECTO_IPV6/, """
[warn] Conditional IPV6 support missing from runtime configuration.

Add the following to your config/runtime.exs:

maybe_ipv6 = if System.get_env("ECTO_IPV6"), do: [:inet6], else: []

config :#{app}, #{app_namespace}.Repo,
...,
socket_options: maybe_ipv6
""")

post_install_instructions("config/runtime.exs", ~r/PHX_SERVER/, """
[warn] Conditional server startup is missing from runtime configuration.

Add the following to your config/runtime.exs:

server? = System.get_env("PHX_SERVER") != nil

config :#{app}, #{app_namespace}.Endpoint,
...,
server: server?
""")

post_install_instructions("config/runtime.exs", ~r/PHX_HOST/, """
[warn] Environment based URL export is missing from runtime configuration.

Add the following to your config/runtime.exs:

host = System.get_env("PHX_HOST") || "example.com"

config :#{app}, #{app_namespace}.Endpoint,
...,
url: [host: host, port: 443]
""")
end

defp ecto_instructions(app) do
"""

# To run migrations
_build/dev/rel/#{app}/bin/migrate
"""
end

defp paths do
[".", :phoenix]
end

defp post_install_instructions(path, matching, msg) do
case File.read(path) do
{:ok, content} ->
unless content =~ matching, do: Mix.shell().info(msg)

{:error, _} ->
Mix.shell().info(msg)
end
end

def otp_vsn do
major = to_string(:erlang.system_info(:otp_release))
path = Path.join([:code.root_dir(), "releases", major, "OTP_VERSION"])

case File.read(path) do
{:ok, content} ->
String.trim(content)

{:error, _} ->
IO.warn("unable to read OTP minor version at #{path}. Falling back to #{major}.0")
"#{major}.0"
end
end
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is gorgeous.

89 changes: 89 additions & 0 deletions priv/templates/phx.gen.release/Dockerfile.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian instead of
# Alpine to avoid DNS resolution issues in production.
#
# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu
# https://hub.docker.com/_/ubuntu?tab=tags
#
#
# This file is based on these images:
#
# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20210902-slim - for the release image
# - https://pkgs.org/ - resource for finding needed packages
# - Ex: hexpm/elixir:<%= elixir_vsn %>-erlang-<%= otp_vsn %>-debian-bullseye-20210902-slim
#
ARG BUILDER_IMAGE="hexpm/elixir:<%= elixir_vsn %>-erlang-<%= otp_vsn %>-debian-bullseye-20210902-slim"
ARG RUNNER_IMAGE="debian:bullseye-20210902-slim"

FROM ${BUILDER_IMAGE} as builder

# install build dependencies
RUN apt-get update -y && apt-get install -y build-essential git \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*

# prepare build dir
WORKDIR /app

# install hex + rebar
RUN mix local.hex --force && \
mix local.rebar --force

# set build ENV
ENV MIX_ENV="prod"

# install mix dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV
RUN mkdir config

# copy compile-time config files before we compile dependencies
# to ensure any relevant config change will trigger the dependencies
# to be re-compiled.
COPY config/config.exs config/${MIX_ENV}.exs config/
RUN mix deps.compile

COPY priv priv

# note: if your project uses a tool like https://purgecss.com/,
# which customizes asset compilation based on what it finds in
# your Elixir templates, you will need to move the asset compilation
# step down so that `lib` is available.
COPY assets assets

# compile assets
RUN mix assets.deploy

# Compile the release
COPY lib lib

RUN mix compile

# Changes to config/runtime.exs don't require recompiling the code
COPY config/runtime.exs config/

COPY rel rel
RUN mix release

# start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
FROM ${RUNNER_IMAGE}

RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*

# Set the locale
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen

ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8

WORKDIR "/app"
RUN chown nobody /app

# Only copy the final release from the build stage
COPY --from=builder --chown=nobody:root /app/_build/prod/rel/<%= otp_app %> ./

USER nobody

CMD /app/bin/server
18 changes: 18 additions & 0 deletions priv/templates/phx.gen.release/dockerignore.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.dockerignore
# there are valid reasons to keep the .git, namely so that you can get the
# current commit hash
#.git
.log
tmp

# Mix artifacts
_build
deps
*.ez
releases

# Generate on crash by the VM
erl_crash.dump

# Static artifacts
node_modules
1 change: 1 addition & 0 deletions priv/templates/phx.gen.release/rel/migrate.bat.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
call "%~dp0\<%= otp_app %>" eval <%= app_namespace %>.Release.migrate
3 changes: 3 additions & 0 deletions priv/templates/phx.gen.release/rel/migrate.sh.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
cd -P -- "$(dirname -- "$0")"
exec ./<%= otp_app %> eval <%= app_namespace %>.Release.migrate
2 changes: 2 additions & 0 deletions priv/templates/phx.gen.release/rel/server.bat.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
set PHX_SERVER=true
call "%~dp0\<%= otp_app %>" start
Loading