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
38 changes: 38 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Build artifacts
_build/
deps/
doc/
cover/

# Test
test/
tasks/
benchmarks/

# Dev / editor
.elixir_ls/
.lexical/
.git/
.github/
.vscode/
.idea/

# Crash dumps and ephemeral files
erl_crash.dump
*.ez
.fetch

# Website-specific paths
website/_build/
website/deps/
website/doc/
website/cover/
website/test/
website/.elixir_ls/
website/erl_crash.dump
website/priv/static/assets/
website/priv/static/cache_manifest.json
website/assets/node_modules/

# Local agent / plan files
.agents/
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,37 @@ jobs:
- name: Format
run: mix format --check-formatted

website:
name: Website tests
runs-on: ubuntu-latest
strategy:
matrix:
include:
- elixir: "1.19.0"
otp: "28.0"
bootstrap_beam: false

steps:
- name: Checkout
uses: actions/checkout@v4
- *setup-beam
- *bootstrap-beam
- name: Restore website dependencies cache
uses: actions/cache@v4
with:
path: website/deps
key: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-website-mix-${{ hashFiles('website/mix.lock') }}
restore-keys: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-website-mix-
- name: Install website dependencies
working-directory: website
run: mix deps.get
- name: Run website tests
working-directory: website
run: mix test
- name: Format
working-directory: website
run: mix format --check-formatted

lua53-suite:
name: Lua 5.3 suite
runs-on: ubuntu-latest
Expand Down
93 changes: 93 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Multi-stage Dockerfile for the deflua.com website.
#
# Build context MUST be the lua/ repo root (parent of website/) so the
# `{:lua, path: ".."}` path dependency is reachable from inside the image.
#
# From the repo root:
# docker build -t deflua .
# fly deploy # fly.toml lives next to this Dockerfile

ARG ELIXIR_VERSION=1.19.4
ARG OTP_VERSION=28.3.3
ARG DEBIAN_VERSION=bookworm-20260518-slim

ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"

FROM ${BUILDER_IMAGE} AS builder

RUN apt-get update -y && apt-get install -y build-essential git curl ca-certificates gnupg \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
&& apt-get update -y && apt-get install -y nodejs \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*

# Mirror the on-disk layout so `{:lua, path: ".."}` in website/mix.exs
# resolves correctly: website lives at /app/website, lua at /app.
WORKDIR /app

ENV MIX_ENV="prod"

RUN mix local.hex --force && mix local.rebar --force

# Lua package: only the files mix needs to compile the dep.
# README.md is read at compile time by lib/lua.ex (@external_resource).
COPY mix.exs mix.lock README.md ./
COPY lib ./lib

# Website mix files first so deps.get is cached when only app code changes.
COPY website/mix.exs website/mix.lock ./website/
WORKDIR /app/website

RUN mix deps.get --only $MIX_ENV
RUN mkdir config

COPY website/config/config.exs website/config/${MIX_ENV}.exs config/
RUN mix deps.compile

# Install npm deps separately from app code so they cache.
# package.json references `../deps/phoenix` etc., so deps/ must already exist
# (it does — `mix deps.get` ran above).
COPY website/assets/package.json website/assets/package-lock.json assets/
RUN cd assets && npm ci --no-audit --no-fund --progress=false

COPY website/priv priv
COPY website/lib lib
COPY website/assets assets

# mix compile must run BEFORE assets.deploy because Phoenix's LiveView
# colocated-hooks compiler generates files under _build/ that esbuild
# resolves via NODE_PATH (`phoenix-colocated/website`).
RUN mix compile
RUN mix assets.deploy

COPY website/config/runtime.exs config/
COPY website/rel rel
# Belt-and-suspenders: ensure the overlay script is exec'able regardless of
# the source-tree mode it was checked out with.
RUN chmod +x rel/overlays/bin/server && mix release

# ---- Runtime image ----
FROM ${RUNNER_IMAGE}

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

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

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

WORKDIR /app
RUN chown nobody /app

ENV MIX_ENV="prod"

COPY --from=builder --chown=nobody:root /app/website/_build/${MIX_ENV}/rel/website ./

USER nobody

CMD ["/app/bin/server"]
61 changes: 61 additions & 0 deletions fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# fly.toml — deflua.com (Lua.ex showcase)
#
# Deploy from the lua/ repo root so the Dockerfile can reach the `:lua` path
# dependency one directory above website/.

app = "deflua"
primary_region = "ewr" # change after `fly platform regions` if desired
kill_signal = "SIGTERM"
kill_timeout = "10s"

[build]
dockerfile = "Dockerfile"

[env]
PHX_HOST = "deflua.com"
PORT = "8080"

[deploy]
strategy = "rolling"

# Single HTTP service — Fly's edge handles TLS termination and HTTP/2.
[[services]]
protocol = "tcp"
internal_port = 8080
auto_stop_machines = "off" # keep machines warm; HN burst can't wait on cold starts
auto_start_machines = true
min_machines_running = 2 # always have spare capacity

# LiveView holds a websocket open for the whole session. Use connection-based
# concurrency so Fly's proxy spreads sockets across machines and triggers
# autoscale before any one node saturates.
[services.concurrency]
type = "connections"
soft_limit = 1000 # start sending new conns to other machines past this
hard_limit = 2000 # refuse new conns past this on a single machine

[[services.ports]]
port = 80
handlers = ["http"]
force_https = true

[[services.ports]]
port = 443
handlers = ["tls", "http"]

# Cheap liveness probe — `/health` is exempt from force_ssl in prod.exs.
[[services.http_checks]]
interval = "15s"
timeout = "5s"
grace_period = "10s"
method = "get"
path = "/health"
protocol = "http"

# Default VM. shared-cpu-2x / 1GB handles a Phoenix LiveView site easily.
# Bump to performance-2x or shared-cpu-4x if BEAM starts saturating.
[[vm]]
size = "shared-cpu-2x"
memory = "1gb"
cpu_kind = "shared"
cpus = 2
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ defmodule Lua.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:tidewave, "~> 0.5", only: [:dev]},
{:usage_rules, "~> 0.1", only: [:dev]},
{:ex_doc, "~> 0.38", only: :dev, runtime: false},
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
Expand Down
Loading
Loading