Problem reading file system from Elixir app

I’m having trouble getting my Elixir app to start on fly.io. I’m using the Dockerfile that flyctl created, although it looks straightforward enough to me. My app has a GenServer application that reads some static files when it starts up. If I docker build the image locally and shell in to it, the files are present (eg. /lib/myapp-0.1.0/priv/vocabs/condition.ttl).

But the logs on fly.io show:

2022-10-27T11:26:12Z app[601b7009] lhr [info]
{"Kernel pid terminated",application_controller,"{application_start_failure,myapp,
{{shutdown,{failed_to_start_child,'Elixir.KnowledgeGraph.Graph',
{#{'__exception__' => true,'__struct__' => 'Elixir.File.Error',
action => <<\"open\">>,
path => <<\"priv/vocabs/condition.ttl\">>, reason => enoent},
[{'Elixir.File','open!',3,[{file,\"lib/file.ex\"},{line,1511}]},
{'Elixir.RDF.Serialization.Reader','do_read_file!',4,
[{file,\"lib/rdf/serialization/reader.ex\"},{line,81}]},

To get enoent, either the file priv/vocabs/condition.ttl didn’t get copied to the image (but I can see that it did, assuming it’s in the right place), or there’s something else I need to do to allow the Elixir process to access the file system in a container.

This is my first time building a Docker image for Elixir, although I’ve done it several times for Ruby and Python apps. But I thought that reading from the file system was usually straightforward. So I’m feeling a bit stuck!

Can you share the code that reads from the priv dir? Thanks!

Sure. My own code is pretty simple:

  defvocab Condition,
    base_iri: "http://example.org/def/core/habitat/",
    file: "condition.ttl"

The docs for defvocab instruct that the path to the vocab file should be relative to priv/vocabs, and indeed this works just fine when he app is running in dev.

The defvocab macro is part of the RDF.ex library. Tracking through the (many) layers, I think we ultimately end up at:

  @spec read_file!(module, Path.t(), keyword) :: Graph.t() | Dataset.t()
  def read_file!(decoder, file, opts \\ []) do
    decoder
    |> Serialization.use_file_streaming!(opts)
    |> do_read_file!(decoder, file, opts)
  end

  defp do_read_file!(false, decoder, file, opts) do
    file
    |> File.open!(file_mode(decoder, opts), &IO.read(&1, @io_read_mode))
    |> case do
      {:error, error} when is_tuple(error) -> error |> inspect() |> raise()
      {:error, error} -> raise(error)
      :eof -> decoder.decode!("", opts)
      content -> decoder.decode!(content, opts)
    end
  end

source

But it does suggest that an obvious debug step for me to do is to try to read that file directly, before defvocab has a go, and log the output.

Sorry, I mean the file argument specifically that you pass to that function. You likely need to be referencing :code.priv_dir when you build the file path.

So my code only has condition.ttl. That gets expanded to priv/vocabs/condition.ttl by the RDF.ex library here:

  defp filename!(opts) do
    if filename = Keyword.get(opts, :file) do
      cond do
        File.exists?(filename) ->
          filename

        File.exists?(expanded_filename = Path.expand(filename, @vocabs_dir)) ->
          expanded_filename

        true ->
          raise File.Error, path: filename, action: "find", reason: :enoent
      end
    end
  end

In my code, I’m passing in %{file: "condition.ttl"} as the opts to that function.

And @vocabs_dir is defined:

  # Note: We're not using :code.priv_dir/1 here on purpose, since vocabulary files should be
  # searched in the vocabs dir of the project in which the vocabulary namespace is defined.
  @vocabs_dir "priv/vocabs"

Tbh, I don’t understand the comment above that definition.

it needs to be referencing @vocabs_dir Path.join(:code.priv_dir(:your_otp_app), "vocabs")

The priv dir of the release is not priv/, it will be something like _build/prod/lib/your_app/priv/, so using :code.priv_dir returns the proper full path to it.

Thanks @chrismccord. I’ll report an issue to the upstream RDF library (that @vocabs_dir attribute declaration is not in my code).