Ammonite fails to start on Ubuntu Docker with nonroot user - how to fix it

Ammonite fails to start on Ubuntu Docker with nonroot user - how to fix it

Recently, I experienced an odd issue starting an Ammonite REPL from a Ubuntu Docker container using a non-root user.
It took me some time to identify and solve the issue and this post will describe the issue and my process to find a solution.

But if you're just interested in a quick solution, here you go:

TLDR - The fix

To fix the issue you must give a name to your newly created non-root user.
For example, if your Dockerfile has something like:

# adds a new non-root user
RUN useradd 10500 -m

you can change to:

# adds a named non-root user
RUN useradd namedUser -u 10500 -m

This simple change will fix the issue with java.lang.IllegalArgumentException: requirement failed: ? is not an absolute path exception.
It seems that Ammonite, and sbt as well, are relying on the OS username for some work.
Another alternative is to switch to an Alpine linux image.

Reproducing the issue

The idea here is simple we need to spin up a ubuntu container with JDK 8, download and install Ammonite, and finally set up a user without sudo privileges to be used on the container.

To make the error easily reproducible I'll share with you a Dockerfile very similar to the one as was used in the occasion, and the necessary commands.

Dockerfile for ammonite console

FROM rightmesh/ubuntu-openjdk:18.04
# installs curl and Ammonite
RUN apt-get update && \
  apt-get install -y curl && \
  sh -c '(echo "#!/usr/bin/env sh" && \
  curl -L https://github.com/lihaoyi/Ammonite/releases/download/2.1.1/2.12-2.1.1) > /usr/local/bin/amm && \
  chmod +x /usr/local/bin/amm'
# adds a non-root user
RUN useradd 10500 -m
# start the container with the non-root user
USER 10500

Steps to reproduce the issue

First, we need to build the image using: docker build -t amm-console .

Then we can reproduce the issue by running:

docker run -it --rm amm-console amm

You should get the following exception as result:

Exception in thread "main" java.lang.ExceptionInInitializerError
  at ammonite.main.Cli$Config$.apply$default$4(Cli.scala:22)
  at ammonite.Main$.main0(Main.scala:294)
  at ammonite.Main$.main(Main.scala:275)
  at ammonite.Main.main(Main.scala)
Caused by: java.lang.IllegalArgumentException: requirement failed: ? is not an absolute path
  at scala.Predef$.require(Predef.scala:281)
  at os.Path.<init>(Path.scala:429)
  at os.Path$.apply(Path.scala:388)
  at os.package$.<init>(package.scala:19)
  at os.package$.<clinit>(package.scala)
  ... 4 more

Identifying the issue

Strategy 1 - Stack trace analysis

After investing some time, with no success, googling for a solution. I decide by investigating the exception to try to find a clue about what's happening.

Caused by: java.lang.IllegalArgumentException: requirement failed: ? is not an absolute path

This error message isn't helpful here, in my opinion, but suggests that something is wrong in a path. Even though we are not explicitly specifying any path.
So I move up on the stack trace until I find this:

at ammonite.main.Cli$Config$.apply$default$4(Cli.scala:22)

So I decided that was time to look on the Ammonite Cli.scala code, more specifically at line 22, and this is how the Cli.:

object Cli{
  //.... more code here
  case class Config(predefCode: String = "",
                    defaultPredef: Boolean = true,
                    homePredef: Boolean = true,
                    wd: os.Path = os.pwd, //this is the line 22
  //.... more code here

code reference here

I don't know about you, but at least for me, this was not answering any question so far. And the frustration has started to build on.

Strategy 2 - bypass the problem changing from Ammonite to sbt

Ok, I realize that I didn't have enough information to solve the Ammonite issue by myself, so I started to ask myself:

Do I need to solve the Ammonite issue or do I need to have a Scala REPL working?

Thanks to that question, I decided by replacing Ammonite by sbt since I could use sbt console and have access to a REPL, and that would solve my problem.

So I changed the Dockerfile to this:

FROM rightmesh/ubuntu-openjdk:18.04

RUN apt-get update && \
  apt-get install -y curl && \
  curl -L -o sbt-1.3.8.deb http://dl.bintray.com/sbt/debian/sbt-1.3.8.deb && \
  dpkg -i sbt-1.3.8.deb && \
  apt-get install sbt

RUN useradd 10500 -m

USER 10500

I built the image again using: docker build -t amm-console .

And try to run sbt executing:

docker run -it --rm amm-console sbt

So... it failed again. The bypass strategy didn't work it out, but now I had a different exception with a more helpful error message, as you can see here:

java.io.IOException: failed to create lock file /?/.sbt/boot/sbt.boot.lock
  at xsbt.boot.Locks$.liftedTree1$1(Locks.scala:35)
  //... more error lines
Caused by: java.io.IOException: No such file or directory
  at java.io.UnixFileSystem.createFileExclusively(Native Method)
  at java.io.File.createNewFile(File.java:1012)
  at xsbt.boot.Locks$.liftedTree1$1(Locks.scala:34)
  ... 12 more

The breakthrough

Comparing both error messages from Ammonite and sbt we can isolate a common element, the mysterious ?.

# -- Ammonite Error
requirement failed: ? is not an absolute path

# -- sbt Error
failed to create lock file /?/.sbt/boot/sbt.boot.lock

Somehow this question mark is being part of my user folder path, and this only happens for my newly created nonroot user 10500.
However, when I run eithersbt or Ammonite docker containers using the root user this error never shows up.
So, it must be something related to this newly created user and his home path.

I decided to run the whoami command for both users root and 10500 and compare the results. And so I did:

# -- user 10500
docker run -it --rm amm-console whoami
--> whoami: cannot find name for user ID 10500

# -- root user
docker run -it --rm amm-console whoami
--> root

As you can see the whoami for user 10500 complains about the fact that this user doesn't have a name.

At least for me, there is no strong evidence that this could be linked to the Ammonite and sbt error, but I decided to give it a try, and to give a name to my user 10500.

So, I changed again my Dockerfile to be like this:

FROM rightmesh/ubuntu-openjdk:18.04

RUN apt-get update && \
  apt-get install -y curl && \
  curl -L -o sbt-1.3.8.deb http://dl.bintray.com/sbt/debian/sbt-1.3.8.deb && \
  dpkg -i sbt-1.3.8.deb && \
  apt-get install sbt

RUN useradd namedUser -u 10500 -m

USER 10500

Once more I build the image using: docker build -t amm-console .

And one more time I ran sbt with:

docker run -it --rm amm-console sbt

And it finally worked 🎉

docker run -it --rm amm-console sbt
[info] [launcher] getting org.scala-sbt sbt 1.3.8  (this may take some time)...

Changing back to Ammonite

Great! Now that it works I can finally move back to Ammonite and check if it will work with my new namedUser.

FROM rightmesh/ubuntu-openjdk:18.04

RUN apt-get update && \
  apt-get install -y curl && \
  sh -c '(echo "#!/usr/bin/env sh" && \
  curl -L https://github.com/lihaoyi/Ammonite/releases/download/2.1.1/2.12-2.1.1) > /usr/local/bin/amm && \
  chmod +x /usr/local/bin/amm'

RUN useradd namedUser -u 10500 -m

USER 10500

So I need to build the image again using: docker build -t amm-console .

An try to start Ammonite REPL with:

docker run -it --rm amm-console amm

And...

docker run -it --rm  amm-console-fix amm
Loading...
Compiling (synthetic)/ammonite/predef/DefaultPredef.sc
Welcome to the Ammonite Repl 2.1.1 (Scala 2.12.11 Java 1.8.0_181)
@

it finally worked 🎉🎉🎉 and I could move my task to done ✅