I’m setting up a docker container to just be a simple environment for Ocaml, since I don’t wanna have to manage two OPAM tool chains on two computers. (Windows desktop, Linux laptop) My goal is to have the container load in to a bash command prompt on docker-compose run with ocaml ready to go, and to do this I need to enter in to bash and then run eval $(opam env) on startup. This is my current docker file:
FROM ocaml/opam:alpine-3.12
# Create folder and assign owner
USER root
RUN mkdir /code
WORKDIR /code
RUN chown opam:opam /code
USER opam
# Install ocaml
RUN opam init
RUN opam switch create 4.11.1
RUN opam install dune
# bash env
CMD [ "/bin/bash" ]
ENTRYPOINT [ "eval", "$(opam env)" ]
Building and trying to run this gives me the error:
sh: $(opam env): unknown operand
ERROR: 2
I tried making a run.sh script but that ran into some chmod/permission issues that are probably harder to debug than this. What do I do to open this container in bash and then run the eval $(opam env) command? I don’t want to do this with command line arguments, I’d like to do this all in a dockerfile or docker-compose file
2
Answers
There’s no way to tell Docker to do something after the main container process has started, or to send input to the main container process.
What you can do is to write a wrapper script that does some initial setup and then runs whatever the main container process is. Since that
eval
command will just set environment variables, those will carry through to the main shell.In the Dockerfile, make this script be the
ENTRYPOINT
:It also might work to put this setup in a shell dotfile, and run
bash -l
as the main container command to force it to read dotfiles. However, the$HOME
directory isn’t usually well-defined in Docker, so you might need to set that variable. If you expand this setup to run a full application, the entrypoint-wrapper approach will will there too, but that sequence probably won’t read shell dotfiles at all.What you show looks like an extremely straightforward installation sequence and I might not change it, but be aware that there are complexities around using version managers in Docker. In particular every Dockerfile
RUN
command has a new shell environment and theeval
command won’t "stick". I’d ordinarily suggest picking a specific version of the toolchain and directly installing it, maybe in/usr/local
, without a version manager, but that approach will be much more complex than what you have currently. For more mainstream languages you can also usually use e.g. anode:16.13
prebuilt image.What’s with the error you’re getting? For
ENTRYPOINT
andCMD
(and alsoRUN
) Docker has two forms. If something is a JSON array then Docker runs the command as a sequence of words, with one word in the array translating to one word in the command, and no additional interpretation or escaping. If it isn’t a JSON array – even if it’s mostly a JSON array, but has a typo – Docker will interpret it as a shell command and run it withsh -c
. Docker applies this rule separately to theENTRYPOINT
andCMD
, and then combines them together into a single command.In particular in your
ENTRYPOINT
line, RFC 8259 §7 defines the valid character escapes in JSON, son
is a newline and so on, but$
is not one of those. That makes the embedded string invalid, and therefore theENTRYPOINT
line isn’t valid, and Docker runs it via a shell. The single main container command is thenwhich runs the shell command
[
, as inif [ "$1" = yes ]; then ...; fi
. That command doesn’t understand the$(...)
string as an argument, which is the error you’re getting.The JSON array already has escaped the things that need to be escaped, so it looks like you could get around this immediate error by removing the erroneous backslash
Docker will run this as-is, combining it with the
CMD
, and you getBut
eval
isn’t a "real" command – there is no/bin/eval
binary – and Docker will pass on the literal string$(opam env)
without interpreting it at all. That’s also not what you want.In principle it’s possible to do this without writing a script, but you lose a lot of flexibility. For example, consider
Again, though, if you replace this
CMD
with anything else you won’t have done the initial setup step.The trick is to use
opam exec
1 as the entry point, e.g.,Then you can either run directly a command from the installed switch or just start an interactive shell with
run -it --rm <cont> sh
and you will have the switch fully activated, e.g.,As an aside, since we’re talking about docker and OCaml, let me share some more tricks. First of all, you can look into our collection of dockerfiles in BAP for some inspiration. And another important trick that I would like to share is using multistage builds to shrink the size of the image, here’s an example Dockerfile. In our case, it gives us a reduction from 7.5 Gb to only 750 Mb, while still preserving the ability to run and build OCaml programs.
And another side note 🙂 You also should run your installation in a single RUN entry, otherwise your layers will eventually diverge and you will get weird missing packages errors. Basically, here’s the Dockerfile that you’re looking for,
1)Or
opam config exec
, i.e.,ENTRYPOINT ["opam", "config", "exec", "--"]
for the older versions of opam.