skip to Main Content

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


  1. 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.

    #!/bin/sh
    # entrypoint.sh
    
    # Set up the version-manager environment
    eval $(opam env)
    
    # Run the main container command
    exec "$@"
    

    In the Dockerfile, make this script be the ENTRYPOINT:

    COPY entrypoint.sh /usr/local/bin
    ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
    CMD ["/bin/bash"]
    

    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 the eval 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. a node:16.13 prebuilt image.


    What’s with the error you’re getting? For ENTRYPOINT and CMD (and also RUN) 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 with sh -c. Docker applies this rule separately to the ENTRYPOINT and CMD, 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, so n is a newline and so on, but $ is not one of those. That makes the embedded string invalid, and therefore the ENTRYPOINT line isn’t valid, and Docker runs it via a shell. The single main container command is then

    sh -c '[ "eval", "$(opam env)" ]' '/bin/bash'
    

    which runs the shell command [, as in if [ "$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

    ENTRYPOINT ["eval", "$(opam env)"] # won't actually work
    

    Docker will run this as-is, combining it with the CMD, and you get

    'eval' '$(opam env)' '/bin/bash'
    

    But 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

    # no ENTRYPOINT; shell-form CMD
    CMD eval $(opam env) && exec /bin/bash
    

    Again, though, if you replace this CMD with anything else you won’t have done the initial setup step.

    Login or Signup to reply.
  2. The trick is to use opam exec1 as the entry point, e.g.,

    ENTRYPOINT ["opam", "exec", "--"]
    

    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.,

    $ docker run -it --rm binaryanalysisplatform/bap:latest sh
    $ which ocaml
    /home/opam/.opam/4.09/bin/ocaml
    

    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,

    FROM ocaml/opam2:alpine
    
    WORKDIR /home/opam
    
    RUN opam switch 4.11.1 
     && eval "$(opam env)" 
     && opam remote set-url default https://opam.ocaml.org 
     && opam update 
     && opam install dune 
     && opam clean -acrs 
    
    ENTRYPOINT ["opam", "exec", "--"]
    

    1)Or opam config exec, i.e., ENTRYPOINT ["opam", "config", "exec", "--"] for the older versions of opam.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search