Phx digest is not working as expected in production

Hey Guys,

I have deployed my learning/tools project on Render.com.

However, I’m running into an issue. I have used custom Fonts for my site, which works in dev and production build locally, however, when I deploy it using Render, it fails.

See: DerpyTools

Expectation:



In digested CSS, the font is referred in this way:
url(/fonts/barriecito-94c9ab0c4e14aa8632e6f0f13bd5dfbb.ttf?vsn=d)

Reality:



In digested CSS, the font is referred in this way:
url(/fonts/barriecito.ttf)

Here’s the build.sh, file used by Render:

#!/usr/bin/env bash
# exit on error
set -o errexit

# Initial setup
mix deps.get --only prod
MIX_ENV=prod mix compile

# Compile assets
MIX_ENV=prod mix assets.deploy

# Build the release and overwrite the existing release directory
MIX_ENV=prod mix release --overwrite

# Run migrations
# _build/prod/rel/derpy_tools/bin/derpy_tools eval "DerpyTools.Release.migrate"

# Run seeds
# MIX_ENV=prod mix run priv/repo/seeds.exs

check the folder where your assets are getting built to, also make sure that folder is served by the static endpoint, you can find the configuration in endpoint.ex

The config is vanilla, I didn’t mess with those.

It’s just that the files get digested and included properly when testing the prod build locally.

But the prod build in Render is somehow not referring to the digested file names.

P.S. Since I’m on the free tier of Render, I can’t SSH into the server and see the files.

Yeah I’ve got a few cases where the config would get messed up, wasted hours to find where the problem was, check that before going any further.

But it might be some kind of limitations, I never used these kind of services as I always deploy on custom VMs, so I am not the best person to ask.

plug Plug.Static,
    at: "/",
    from: :derpy_tools,
    gzip: false,
    only: DerpyToolsWeb.static_paths()

Yeah those static paths ( DerpyToolsWeb.static_paths() ) don’t look default to me, however I didn’t use phoenix in quite long time, maybe they define paths in a different module now.

def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)

To make the production build locally, I’m doing:

build.sh

#!/usr/bin/env bash
# exit on error
set -o errexit

# Initial setup
mix deps.get --only prod
MIX_ENV=prod mix compile

# Compile assets
MIX_ENV=prod mix assets.deploy

# Build the release and overwrite the existing release directory
MIX_ENV=prod mix release --overwrite

To Build

bash build.sh

To Run

_build/prod/rel/derpy_tools/bin/derpy_tools eval "DerpyTools.Release.migrate" && _build/prod/rel/derpy_tools/bin/derpy_tools start

Exactly the same commands that are being used on Render.

In local prod build, the file digestion works as expected and files have gibberish names like:
url(/fonts/barriecito-94c9ab0c4e14aa8632e6f0f13bd5dfbb.ttf?vsn=d)

In render prod build, the file digestion doesn’t work as expected, and not all files have gibberish name:
url(/fonts/barriecito.ttf)


I don’t see how that’s the config’s fault. Maybe I’m missing missing something in the bash script, or I don’t know.

It seems to be the case, I don’t remember what those hashes are used after the name for, but it would be definitely a red flag if they were to appear in production as things such as subresources integrity are in a different format: Subresource Integrity - Security on the web | MDN

Those hashes are used for cache bursting.

Say the browser caches a resource named app.css. Next time, if I make changes to some CSS in app.css. The browser will continue to pick the cached value.

However, with modern build tools, hashes are attached to the end of file names, i.e. app-gibberish-hash.css. So the browser will download the modified CSS, and the latest changes will show up in the front end.


Subresource Integrity is a different thing. It’s like checking if the javascript we linked to in our site, is indeed the original one and has not been tampered with.

1 Like

Quite a primitive way to bust cache, but if that is the case, I wonder how the server knows what cache version needs to be served?

It’s not a primitive way of doing this. It’s the defacto way. Kind of like an E-tag.

The server just needs to serve the latest version, and browsers always ask for the latest version based on the file name mentioned in the HTML.

!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="J0t4OwEVaQNcUxIKIhsIdQw7MxsYBShOq8LZDJ_1l9GMTjaMToEzhce-">
    <title data-suffix=" · Phoenix Framework">
       DerpyTools · Phoenix Framework
    </title>
    <link phx-track-static rel="stylesheet" href="/assets/app-261e735eb14c49658241f5f9ee54eaa1.css?vsn=d">
    <script defer phx-track-static type="text/javascript" src="/assets/app-125d7e02817e6708060d18fbd2814fe6.js?vsn=d">
    </script>
    <script>
      localStorage.getItem("dark_mode") === "true" && document.documentElement.classList.add("dark");
    </script>
  </head>
  <body>
  </body>
</html>

In this case, the CSS file is supposed to contain the hashed names, as it does in the local prod build.

assets/app-261e735eb14c49658241f5f9ee54eaa1.css?vsn=d

@font-face{
  font-display: swap;
  font-family: Barriecito;
  font-style: normal;
  font-weight:100 900;
  src: url(/fonts/barriecito.ttf)
}
...

In local prod build:

<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="PAs4ZH0ybAVeJH8Fa3EDOAZiaAI1aGIjTYu0Df9b6v16RG7Zm3-nEEWu">
    <title data-suffix=" · Phoenix Framework">
       DerpyTools · Phoenix Framework
    </title>
    <link phx-track-static rel="stylesheet" href="/assets/app-261e735eb14c49658241f5f9ee54eaa1.css?vsn=d">
    <script defer phx-track-static type="text/javascript" src="/assets/app-125d7e02817e6708060d18fbd2814fe6.js?vsn=d">
    </script>
    <script>
      localStorage.getItem("dark_mode") === "true" && document.documentElement.classList.add("dark");
    </script>
  </head>
  <body>
  </body>
</html>

assets/app-261e735eb14c49658241f5f9ee54eaa1.css?vsn=d

@font-face{
  font-display: swap;
  font-family: Barriecito;
  font-style: normal;
  font-weight:100 900;
  src: url(/fonts/barriecito-94c9ab0c4e14aa8632e6f0f13bd5dfbb.ttf?vsn=d)
}
...

Here’s how the digested files look:

1 Like

Mate, I’m out: "The Mess We're In" by Joe Armstrong - YouTube

Thanks for trying to help @D4no0. :upside_down_face:

No problem, I have a feeling that browser is doing too many things these days, and it will end the same way as the other concurrents, there is so much you can abstract, as the implementation will always differ, and shifting this into the browser territory is a dangerous game.

@LostKobrakai,

I see on your website, you have a properly hashed font. Can you help me out?

KobraKai

url(/font/noway-regular-webfont-f9fbb43af88b754b0327e6d09840962c.woff2?vsn=d

I now tried deploying using the Dockerfile.

It still breaks, and the fonts don’t get digested. :sob:

# 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-20230612-slim - for the release image
#   - https://pkgs.org/ - resource for finding needed packages
#   - Ex: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bullseye-20230612-slim
#
ARG ELIXIR_VERSION=1.15.4
ARG OTP_VERSION=26.0.2
ARG DEBIAN_VERSION=bullseye-20230612-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

# 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

COPY lib lib

COPY assets assets

# compile assets
RUN mix assets.deploy

# Compile the release
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

# set runner ENV
ENV MIX_ENV="prod"

# Only copy the final release from the build stage
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/derpy_tools ./

USER nobody

CMD ["sh", "-c", "/app/bin/migrate && /app/bin/server"]


app.css

@import "fonts.css";

@import "tailwindcss/base";
@import "./base.css";

@import "tailwindcss/components";
@import "./components.css";

@import "tailwindcss/utilities";

@import "./pages.css";

fonts.css

@font-face {
  font-family: "Barriecito";
  font-weight: 100 900;
  font-display: swap;
  font-style: normal;
  src: url("/fonts/barriecito.ttf");
}

@font-face {
  font-family: "Inter";
  font-weight: 100 900;
  font-display: swap;
  font-style: normal;
  src: url("/fonts/inter.ttf");
}

@font-face {
  font-family: "Poppins";
  font-weight: 900;
  font-display: swap;
  font-style: normal;
  src: url("/fonts/poppins/poppins-black.ttf");
}

@font-face {
  font-family: "Poppins";
  font-weight: 900;
  font-display: swap;
  font-style: italic;
  src: url("/fonts/poppins/poppins-black-italic.ttf");
}
...

Even the images url got hashed properly, but not the fonts.

image

Even the cache manifest file has hashed values, but the CSS file doesn’t refer to them:

@chrismccord,

Will it be considered a bug, or am I missing something?

Manifest