Recommended way to handle empty vs existing DB in Dockerfile - docker

I want to run M/Monit (https://mmonit.com/) in a docker container and found this Dockerfile: https://github.com/mlebee/docker-mmonit/blob/master/Dockerfile
I'm using it with a simple docker-compose.yml in my test environment:
version: '3'
services:
mmonit:
build: .
ports:
- "8080:8080"
#volumes:
#- ./db/:/opt/mmonit/db/
It does work, but I want to extend the Dockerfile so that the path /opt/mmonit/db/ is exported as a volume. I'm struggling to implement the following behaviour:
When the volume mapped to /opt/mmonit/db/ is empty (for example on first setup) the files from the install archive should be written to the volume. The db folder is part of the archive.
When the database file /opt/mmonit/db/mmonit.db already exists in the volume, it should not be overwritten in any circumstances.
I do have an idea how to script the required operations / checks in bash, but I'm not even sure if it would be better to replace the ENTRYPOINT with a custom start script or if it should be done by modifying the Dockerfile only.
That's why I ask for the recommended way.

In general the strategy you lay out is the correct path; it's essentially what the standard Docker Hub database images do.
The image you link to is a community image, so you shouldn't feel particularly bound to that image's decisions. Given the lack of any sort of license file in the GitHub repository you may not be able to copy it as-is, but it's also not especially complex.
Docker supports two "halves" of the command to run, the ENTRYPOINT and CMD. CMD is easy to provide on the Docker command line, and if you have both, Docker combines them together into a single command. So a very typical pattern is to put the actual command to run (mmmonit -i) as the CMD, and have the ENTRYPOINT be a wrapper script that does the required setup and then exec "$#".
#!/bin/sh
# I am the Docker entrypoint script
# Create the database, but only if it does not already exist:
if ! test -f /opt/mmonit/db/mmonit.db; then
cp -a /opt/monnit/db_base /opt/monnit/db
fi
# Replace this script with the CMD
exec "$#"
In your Dockerfile, then, you'd specify both the CMD and ENTRYPOINT:
# ... do all of the installation ...
# Make a backup copy of the preinstalled data
RUN cp -a db db_base
# Install the custom entrypoint script
COPY entrypoint.sh /opt/monit/bin
RUN chmod +x entrypoint.sh
# Standard runtime metadata
USER monit
EXPOSE 8080
# Important: this must use JSON-array syntax
ENTRYPOINT ["/opt/monit/bin/entrypoint.sh"]
# Can be either JSON-array or bare-string syntax
CMD /opt/monit/bin/mmonit -i
I would definitely make these kind of changes in a Dockerfile, either starting FROM that community image or building your own.

Related

Should Dockerfiles specify an entry point when using docker compose?

I am migrating some web-apps to be managed via docker compose
It seems the docker-compose.yaml has a section for the container entry-point.
However, my individual docker files have an ENTRYPOINT themselves... should I remove this from the Dockerfiles? Does the entry-point in docker-compose override the Docker one?
You usually shouldn't specify entrypoint: or command: in a Compose file. Prefer specifying these in a Dockerfile. The one big exception is if you have a container that can do multiple things (for example, it can be both a Web server and a queue worker, with the same code) and you need to tell it with a command: to do not-the-default thing.
I'd suggest a typical setup like:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
# with neither entrypoint: nor command:
# Dockerfile
FROM ...
WORKDIR /app
COPY ...
RUN ...
# ENTRYPOINT ["./entrypoint-wrapper.sh"]
CMD ["./my_app"]
Compose entrypoint: overrides the Dockerfile ENTRYPOINT and resets the CMD. Compose command: overrides the Dockerfile CMD.
In the Dockerfile both ENTRYPOINT and CMD are optional. If your base image already includes a correct command setup (nginx, php:fpm) then you can safely skip both.
It's otherwise somewhat a matter of style whether to use CMD or ENTRYPOINT in your Dockerfile. I prefer CMD for two reasons: it's easier to replace in a docker run ... image-name alternate command invocation, and there's a pattern of using ENTRYPOINT as a wrapper script to do first-time setup and then launch the CMD with exec "$#". If you have a JSON-array-syntax ENTRYPOINT then you can pass additional command-line arguments to it as docker run ... image-name --option. Both setups are commonplace.
The thing you shouldn't do is put an interpreter in ENTRYPOINT and a script name in CMD. I only ever see this in Python, but ENTRYPOINT ["python3"] is wrong. On the one hand this is hard to override in the same way ENTRYPOINT is in general, and on the other neither normal command override format works (you still have to repeat the script name if you want to run the same script with different options).

CMD in dockerfile vs command in docker-compose.yml

What is the difference?
Which is preferred?
Should CMD be omitted if command is defined?
command overrides the CMD in dockerfile.
If you control the dockerfile yourself, put it there. It is the cleanest way.
If you want to test something or need to alter the CMD while developing it is faster than always changing the dockerfile and rebuild the image.
Or if it is a prebuilt image and you don't want to build a derivate FROM ... image just to change the CMD it is also a quick solution doing it by command.
In the common case, you should have a Dockerfile CMD and not a Compose command:.
command: in the Compose file overrides CMD in the Dockerfile. There are some minor syntactic differences (notably, Compose will never automatically insert a sh -c shell wrapper for you) but they control the same thing in the container metadata.
However, remember that there are other ways to run a container besides Compose. docker run won't read your docker-compose.yml file and so won't see that command: line; it's also not read in tools like Kubernetes. If you build the CMD into the image, it will be honored in all of these places.
The place where you do need a command: override is if you need to launch a non-default main process for a container.
Imagine you're building a Python application. You might have a main Django application and a Celery worker, but these have basically the same source code. So for this setup you might make the image's CMD launch the Django server, and override command: to run a Celery worker off the same image.
# Dockerfile
# ENTRYPOINT is not required
CMD ["./manage.py", "runserver", "0.0.0.0:8080"]
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports: ['8080:8080']
# no command:
worker:
build: .
command: celery worker

Best way to update config file in Docker with environment variables

im unable to find an easy solution, but probably i'm just searching for the wrong things:
I have a docker-compose.yml which contains a tomcat that is built by the contents of the /tomcat folder. In /tomcat there is a Dockerfile, a .war and a server.xml.
The Dockerfile is based on tomcat:9, and copys the server.xml and .war files into the right directories.
If I do docker-compose up, everything is running fine. But i would love to find a way to update the connectors within the server.xml, without pruning the image, adjusting the server.xml and start it again.
It would be perfect to put a $CONNECTOR_CONFIG in the server.xml, and provide an variables.env to docker-compose where the $CONNECTOR_CONFIG variable is set to like ""
I know i could adjust the server.xml within the Dockerfile with sed, but this way the image must be pruned everytime i want to change something right?
Is there a way that i can later just edit the variables.env and docker-compose down/up?
Regards,
EdFred
A useful pattern here is to use the image's ENTRYPOINT as a wrapper script that does first-time setup. If that script ends with exec "$#" then it will execute the image's CMD as normal. You can use this to do things like rewrite configuration files based on environment variables.
#!/bin/sh
# docker-entrypoint.sh
# Replace any environment variable references in server.xml.tmpl.
# (Assumes the image has the full GNU tool set.)
envsubst <"$CATALINA_BASE/conf/server.xml.tmpl" >"$CATALINA_BASE/conf/server.xml"
# Run the standard container command.
exec "$#"
Normally in a tomcat image you wouldn't include a CMD since the base image knows how to start Tomcat. The Docker Hub tomcat image page has a mention of it, or you can click through to find the original Dockerfile. You need to know this since specifying an ENTRYPOINT in a derived Dockerfile will reset the CMD.
Your Dockerfile then needs to COPY this script in and set up the ENTRYPOINT and CMD.
# Dockerfile
FROM tomcat:9
COPY myapp.war /usr/local/tomcat/webapps/
COPY server.xml.tmpl /usr/local/tomcat/conf/
COPY docker-entrypoint.sh /usr/local/tomcat/bin/
# ENTRYPOINT _MUST_ be JSON-array form
ENTRYPOINT ["docker-entrypoint.sh"]
# Duplicate from base image
CMD ["catalina.sh", "run"]
You can verify this by hand using a docker run command. Any command you specify after the image name gets run instead of the CMD; but the main container command is still constructed by passing that command as arguments to the alternate ENTRYPOINT and so your wrapper script will run.
docker run --rm \
-e CONNECTOR_CONFIG=test-connector-config \
my-image \
cat /usr/local/tomcat/conf/server.xml
In your final Compose setup, you can include the configuration as an environment: variable.
version: '3.8'
services:
myapp:
build: .
ports: ['8080:8080']
environment:
CONNECTOR_CONFIG: ...
envsubst is a GNU tool that replaces $ENVIRONMENT_VARIABLE references in text files. It's very useful for this specific case, but you can do the same work with sed or another text-processing tool, especially if you don't have the GNU tools available (in particular if you have an Alpine-based image).

Can the default location of the Traefik configuration file be changed in the official Docker file?

I have a non-critical Docker Compose project where the Traefik rules vary acceptably between dev and production (I need Lets Encrypt on prod, but not on dev). I am using the [file] config provider.
Currently I am creating separate builds for dev and prod, thus:
# This is fetched from the Compose config
ARG BUILD_NAME
RUN if [ "$BUILD_NAME" == "prod" ]; then \
echo Compiling prod config... ; \
sh /root/compile/prod.sh > /etc/traefik/traefik.toml ; \
else \
echo Compiling dev config... ; \
sh /root/compile/dev.sh > /etc/traefik/traefik.toml ; \
fi
While this project is not enormously important, per-env builds is a bit hacky, and I'd rather go with the standard container approach of one image for all environments.
To do that, I was thinking of doing something like this:
FROM traefik:1.7
# This is set in the Docker Compose config
ENV ENV_NAME
# Let's have a sig handler
RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64
RUN chmod +x /usr/local/bin/dumb-init
COPY docker/traefik/start.sh /root/start.sh
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
CMD ["/root/start.sh"]
The start.sh would have something that would run my "compile" shell command at run time (this selects pieces of config based on the environment). However, the official Traefik images do not run a shell - there are a compiled blob from Go source - so this won't work. Is there a env var by which /etc/traefik/traefik.toml can be changed, or an industry standard way of doing this in Docker?
I did think of using volumes, but that means the container won't "plug-n-play" without additional set up - I like that it is currently self-contained. However, I may use that if there is no alternative. I could run the config "compiler" on the host.
Another approach is to install Traefik in an image that has a shell - maybe it would work with Alpine. I am not sure how I feel about that - removing the shell is a good security feature, so I am hesitant to add it back in, even if I don't think it can be easily exploited.
I didn't find a way to modify the Traefik config file path using environment variables. However, I hit on a volume-based solution that seems to be quite self-contained.
I set up another image called shell in my Docker Compose file:
shell:
build:
context: docker/shell
volumes:
# On-host volume for generating config
- "./docker/traefik/compiled:/root/compiled-host"
This features a bind-mount volume to catch generated config files.
Next, I created a Dockerfile for the new service:
FROM alpine:3.10
COPY compile /root/compile
COPY config /root/config
# Compile different versions of the config, ready to copy into an on-host volume
RUN mkdir /root/compiled && \
sh /root/compile/dev.sh > /root/compiled/traefik-dev.toml && \
sh /root/compile/prod.sh > /root/compiled/traefik-prod.toml
This will create config files for both environments as part of the built image.
When Docker Compose is started up, this service will briefly start, but it will soon exit gracefully and harmlessly. It is intended to be run on an ad-hoc basis anyway.
I already had environment-specific YAML config files, docker-compose-dev.yml and docker-compose-prod.yml, which are explicitly specified in the Compose command with -f. I then used this file to expose the generated on-host file to Traefik. Here's the dev:
traefik:
volumes:
# On-host volume for config
- "./docker/traefik/compiled/traefik-dev.toml:/etc/traefik/traefik.toml"
Much the same was done for traefik-prod.toml
Then, I created per-env commands to copy the config from the shell image into the on-host volume:
#!/bin/sh
docker-compose -f docker-compose.yml -f docker-compose-prod.yml run shell cp /root/compiled/traefik-prod.toml /root/compiled-host/traefik-prod.toml
Finally, when Traefik starts as part of the Compose application, it will find its configuration file in its usual place, /etc/traefik/traefik.toml, but this is in fact a file volume to the generated copy on the host.

How to create a properties file via Dockerfile with dynamic values passed in docker run?

I am relatively new to Docker. Maybe this is a silly question.
My goal is to create an image which has a system.properties file, which as the name says, is a properties file with key value pairs.
I want to fill the values in this file dynamically. So I think the values need to be passed as environment variables to the Docker run command.
For example, if this is what i want in my system.properties file:
buildMode=true
source=/path1
I want to provide the values to this file dynamically, something like:
$ docker run -e BUILD_MODE=true -e SOURCE='/path1' my_image
But I'm stuck at how I can copy the values into the file. Any help will be appreciated.
Note: Base image is linux centos.
As you suspect, you need to create the actual file at runtime. One pattern that’s useful in Docker is to write a dedicated entrypoint script that does any required setup, then launches the main container command.
If you’re using a “bigger” Linux distribution base, envsubst is a useful tool for this. (It’s part of the GNU toolset and isn’t available by default on Alpine base images, but on CentOS it should be.) You might write a template file:
buildMode=${BUILD_MODE}
source=${SOURCE}
Then you can copy that template into your image:
...
WORKDIR /app
COPY ...
COPY system.properties.tmpl ./
COPY entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]
CMD ["java", "-jar", "app.jar"]
The entrypoint script needs to run envsubst, then go on to run the command:
#!/bin/sh
envsubst <system.properties.tmpl >system.properties
exec "$#"
You can do similar tricks just using sed(1), which is more universally available, but requires potentially trickier regular expressions.

Resources