Skip to content

Instantly share code, notes, and snippets.

@mikehostetler
Created November 8, 2022 14:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikehostetler/87fabdce8d72543a32b295e180eada0f to your computer and use it in GitHub Desktop.
Save mikehostetler/87fabdce8d72543a32b295e180eada0f to your computer and use it in GitHub Desktop.
Elixir + Rust + ESBuild + SASS + Tailwind deploy setup to Fly.io
name: "Deploy Server Prod"
on:
workflow_dispatch:
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
jobs:
deploy:
name: Deploy Server to Production
runs-on: ubuntu-18.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Docker layer caching
uses: satackey/action-docker-layer-caching@v0.0.11
continue-on-error: true
- name: Build app Docker image
run: docker build ./fitclub_server -t fitclub
- uses: superfly/flyctl-actions@1.1
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
FLY_PROJECT_PATH: "./fitclub_server"
with:
args: "deploy -a hrmfitclub-prod --config ./fly.toml"
app = "hrmfitclub-prod"
kill_signal = "SIGTERM"
kill_timeout = 5
[env]
[deploy]
release_command = "/app/bin/fitclub eval Fitclub.Release.migrate"
[[services]]
internal_port = 4000
protocol = "tcp"
[services.concurrency]
hard_limit = 25
soft_limit = 20
[[services.ports]]
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]]
grace_period = "30s" # allow some time for startup
interval = "15s"
restart_limit = 6
timeout = "2s"
# Using the Hex.pm docker images. You have much better version control for
# Elixir, Erlang and Alpine.
#
# - https://hub.docker.com/r/hexpm/elixir/tags
# - Ex: hexpm/elixir:1.11.2-erlang-23.3.2-alpine-3.13.3
#
# Debugging Notes:
#
# docker build -it --rm hello_elixir /bin/ash
# docker run -it --rm hello_elixir /bin/ash
###
### Fist Stage - Building the Release
###
FROM hexpm/elixir:1.13.3-erlang-24.2.1-alpine-3.15.0 AS build
# install build dependencies
RUN apk add --no-cache build-base npm yarn git ca-certificates gcc
# Add glibc for dart_sass - https://github.com/CargoSense/dart_sass#faq
RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.34-r0/glibc-2.34-r0.apk
RUN apk add glibc-2.34-r0.apk
# Install Rust for MJML
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH \
RUST_VERSION=1.56.0
RUN set -eux; \
apkArch="$(apk --print-arch)"; \
case "$apkArch" in \
x86_64) rustArch='x86_64-unknown-linux-musl'; rustupSha256='bdf022eb7cba403d0285bb62cbc47211f610caec24589a72af70e1e900663be9' ;; \
aarch64) rustArch='aarch64-unknown-linux-musl'; rustupSha256='89ce657fe41e83186f5a6cdca4e0fd40edab4fd41b0f9161ac6241d49fbdbbbe' ;; \
*) echo >&2 "unsupported architecture: $apkArch"; exit 1 ;; \
esac; \
url="https://static.rust-lang.org/rustup/archive/1.24.3/${rustArch}/rustup-init"; \
wget "$url"; \
echo "${rustupSha256} *rustup-init" | sha256sum -c -; \
chmod +x rustup-init; \
./rustup-init -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION --default-host ${rustArch}; \
rm rustup-init; \
chmod -R a+w $RUSTUP_HOME $CARGO_HOME; \
rustup --version; \
cargo --version; \
rustc --version;
# prepare build dir
WORKDIR /app
# extend hex timeout
ENV HEX_HTTP_TIMEOUT=20
# install hex + rebar
RUN mix local.hex --force && \
mix local.rebar --force
# set build ENV as prod
ENV MIX_ENV=prod \
SECRET_KEY_BASE=nokey \
RUSTFLAGS='--codegen target-feature=-crt-static'
# Copy over the mix.exs and mix.lock files to load the dependencies. If those
# files don't change, then we don't keep re-fetching and rebuilding the deps.
COPY mix.exs mix.lock ./
COPY config config
RUN mix deps.get --only prod && \
mix deps.compile
# install npm dependencies
COPY assets/package.json assets/yarn.lock ./assets/
# RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
RUN yarn --cwd ./assets --no-progress --non-interactive
COPY priv priv
COPY assets assets
# NOTE: If using TailwindCSS, it uses a special "purge" step and that requires
# the code in `lib` to see what is being used. Uncomment that here before
# running the npm deploy script if that's the case.
COPY lib lib
# build assets
# RUN npm run --prefix ./assets deploy
RUN mix assets.deploy
# copy source here if not using TailwindCSS
# COPY lib lib
# compile and build release
COPY rel rel
RUN mix do compile, release
###
### Second Stage - Setup the Runtime Environment
###
# prepare release docker image
FROM alpine:3.15.0 AS app
RUN apk add --no-cache libstdc++ openssl ncurses-libs imagemagick file
WORKDIR /app
RUN chown nobody:nobody /app
USER nobody:nobody
COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/fitclub ./
ENV HOME=/app
ENV MIX_ENV=prod
ENV SECRET_KEY_BASE=nokey
ENV PORT=4000
CMD ["bin/fitclub", "start"]
defmodule Fitclub.MixProject do
use Mix.Project
@version "0.12.8"
def project do
[
app: :fitclub,
version: @version,
elixir: "~> 1.13",
elixirc_paths: elixirc_paths(Mix.env()),
elixirc_options: [
# warnings_as_errors: true,
debug_info: Mix.env() == :dev
],
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
# dialyzer: [plt_add_apps: [:ex_unit, :mix], ignore_warnings: "config/dialyzer.ignore"],
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
]
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {Fitclub.Application, []},
extra_applications: [:ex_machina, :logger, :runtime_tools]
]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
# Configuration Management - Upstream confix has issue with Mix Releases
{:confix, git: "https://github.com/mikehostetler/confix.git"},
# Monitoring
{:appsignal, "~> 2.0"},
{:appsignal_phoenix, "~> 2.0.0"},
{:sentry, "~> 8.0"},
{:prom_ex, "~> 1.6.0"},
# Absinthe for GraphQL
{:absinthe, "~> 1.7.0", override: true},
{:absinthe_plug, "~> 1.5.5"},
{:absinthe_relay, "~> 1.5.0"},
{:absinthe_error_payload, "~> 1.0"},
{:dataloader, "~> 1.0.0"},
# Background Jobs
{:oban, "~> 2.9"},
# Kaffy administration
{:kaffy, "~> 0.9.0"},
{:guardian, "~> 2.0"},
{:phoenix, "~> 1.6.0", override: true},
{:phoenix_ecto, "~> 4.1"},
{:ecto_sql, "~> 3.4"},
{:postgrex, ">= 0.0.0"},
{:phoenix_live_view, "~> 0.17.0", override: true},
{:phoenix_html, "~> 3.0", override: true},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_dashboard, "~> 0.5"},
{:brady, "~> 0.0.10"},
{:heroicons_liveview, "~> 0.5.0"},
# Asset pipeline
{:esbuild, "~> 0.2", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.1", runtime: Mix.env() == :dev},
{:dart_sass, "~> 0.4", runtime: Mix.env() == :dev},
# Monitoring
{:telemetry, "~> 1.0", override: true},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0", override: true},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},
{:plug_canonical_host, "~> 2.0"},
# Pagination & Filtering
{:scrivener_ecto, "~> 2.7"},
{:filtrex, "~> 0.4.3"},
# Policies
{:bodyguard, "~> 2.4"},
# Caching
{:cachex, "~> 3.4"},
# Date Time management
{:castore, "~> 0.1.10"},
{:mint, "~> 1.3"},
{:tz, "~> 0.20.1"},
{:tz_extra, "~> 0.20.1"},
{:timex, "~> 3.0"},
# SVG Charting
{:contex, "~> 0.4.0"},
# Data Mocking
{:ex_machina, "~> 2.7.0"},
{:faker, "~> 0.17.0"},
# Stripe for Payments
{:stripity_stripe, "~> 2.0"},
# Bamboo for Emailing
{:bamboo, "~> 2.0"},
{:bamboo_phoenix, "~> 1.0"},
{:premailex, "~> 0.3.0"},
{:floki, ">= 0.0.0"},
{:mjml, "~> 1.2.0"},
# Waffle for file upload
{:waffle, "~> 1.1.1"},
{:waffle_ecto, "~> 0.0.9"},
# If using Waffle with S3:
{:ex_aws, "~> 2.2.3"},
{:ex_aws_s3, "~> 2.0"},
{:hackney, "~> 1.9"},
{:sweet_xml, "~> 0.6"},
{:phx_gen_auth, "~> 0.6", only: [:dev], runtime: false},
{:bcrypt_elixir, "~> 3.0"},
{:ecto_psql_extras, "~> 0.2"},
{:hashids, "~> 2.0"},
# Intercom
{:intercom, "~> 2.0", hex: :intercom_elixir},
# CSV parsing
{:csv, "~> 2.4"},
# Testing
{:assertions, "~> 0.19.0", only: [:test]},
{:ex_check, "~> 0.14.0", only: [:dev, :test], runtime: false},
{:credo, ">= 0.0.0", only: [:dev, :test], runtime: false},
{:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false},
{:doctor, ">= 0.0.0", only: [:dev, :test], runtime: false},
{:ex_doc, ">= 0.0.0", only: [:dev, :test], runtime: false},
{:sobelow, ">= 0.0.0", only: [:dev, :test], runtime: false}
]
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to install project dependencies and perform other setup tasks, run:
#
# $ mix setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
setup: ["setup.deps", "ecto.setup"],
"setup.deps": ["deps.get", "cmd yarn --prefix assets", "assets.deploy"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.drop --quiet", "ecto.create --quiet", "ecto.migrate --quiet", "test"],
sentry_recompile: ["compile", "deps.compile sentry --force"],
"assets.deploy": [
"esbuild default --minify",
"sass default --no-source-map --style=compressed",
"tailwind default --minify",
"phx.digest"
]
]
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment