Docker multiple environments - docker

I'm trying to wrap my head around Docker, but I'm having a hard time figuring it out. I tried to implement it in my small project (MERN stack), and I was thinking how do you distinct between development, (maybe staging), and production environments.
I saw one example where they used 2 Docker files, and 2 docker-compose files, (each pair for one env, so Dockerfile + docker-compose.yml for prod, Dockerfile-dev + docker-compose-dev.yml for dev).
But this just seems like a bit of an overkill for me. I would prefer to have it only in two files.
Also one of the problem is that e.g. for development I want to install nodemon globally, but not for poduction.
In perfect solution I imagine running something like that
docker-compose -e ENV=dev build
docker-compose -e ENV=dev up
Keep in mind, that I still don't fully get docker, so if you caught some of mine misconceptions about docker, you can point them out.

You could take some clues from "Using Compose in production"
You’ll almost certainly want to make changes to your app configuration that are more appropriate to a live environment. These changes may include:
Removing any volume bindings for application code, so that code stays inside the container and can’t be changed from outside
Binding to different ports on the host
Setting environment variables differently (e.g., to decrease the verbosity of logging, or to enable email sending)
Specifying a restart policy (e.g., restart: always) to avoid downtime
Adding extra services (e.g., a log aggregator)
The advice is then not quite similar to the example you mention:
For this reason, you’ll probably want to define an additional Compose file, say production.yml, which specifies production-appropriate configuration. This configuration file only needs to include the changes you’d like to make from the original Compose file.
docker-compose -f docker-compose.yml -f production.yml up -d
This overriding mechanism is better than trying to mix dev and prod logic in one compose file, with environment variable to try and select one.
Note: If you name your second dockerfile docker-compose.override.yml, a simple docker-compose up would read the overrides automatically.
But in your case, a name based on the environment is clearer.

Docker Compose will read docker-compose.yml and docker-compose.override.yml by default. Understanding-Multiple-Compose-Files
You can set a default docker-compose.yml and different overwrite compose file. For example, docker-compose.prod.yml docker-compose.test.yml. Keep them in the same place.
Then create a symbolic link named docker-compose.override.yml for each env.
Track docker-compose.{env}.yml files and add docker-compose.override.yml to .gitignore.
In prod env: ln -s ./docker-compose.prod.yml ./docker-compose.override.yml
In test env: ln -s ./docker-compose.test.yml ./docker-compose.override.yml
The project structure will then look like this:
project\
- docker-compose.yml # tracked
- docker-compose.prod.yml # tracked
- docker-compose.test.yml # tracked
- docker-compose.override.yml # ignored & linked to override composefile for current env
- src/
- ...
Then you have done. In each environment, you can use the compose-file with the same command docker-compose up
If you are not sure, use docker-compose config to check if it's been override properly.

Related

How to name docker-compose files

How do you name your docker-compose.yml files? Is there a convention to follow?
With Dockerfiles <purpose>.Dockerfile seems to work the best being instantly recognized by VSCode and PyCharm.
I like the idea of structuring docker/compose files into folders so that default names could be used, but as far as I know docker can't see files up the tree creating different problems.
According to the -f documentation:
If you don’t provide this flag on the command line, Compose [...] looks for a
docker-compose.yml and a docker-compose.override.yml file.
If you don't want to use the -f flag you can use the default name docker-compose.yml and override it with docker-compose.override.yml.
However, if you use -f, since you'll provide the filename, you can use whatever you want.
I think a good way to name them depending on the environment could be docker-compose.{env}.yml and place them at the top level directory:
docker-compose.prod.yml
docker-compose.dev.yml
docker-compose.test.yml
docker-compose.staging.yml
And you can use the default docker-compose.yml to define the base configuration that is common to all environments.

Conditionally mount volumes in docker-compose for several conditions

I use docker and docker compose to package scientific tools into easily/universally executable modules. One example is a docker that packages a rather complicated python library into a container that runs a jupyter notebook server; the idea is that other scientists who are not terribly tech-savvy can clone a github repository, run docker-compose up then do their analyses without having to install the library, configure various plugins and other dependencies, etc.
I have this all working fine except that I'm having issues getting the volume mounts to work in a coherent fashion. The reason for this is that the library inside the docker container handles multiple kinds of datasets, which users will store in several separate directories that are conventionally tracked through shell environment variables. (Please don't tell me this is a bad way to do this--it's the way things are done in the field, not the way I've chosen to do things.) So, for example, if the user stores FreeSurfer data, they will have an environment variable named SUBJECTS_DIR that points to the directory containing the data; if they store HCP data, they will have an environment variable HCP_SUBJECTS_DIR. However, they may have both, either, or neither of these set (as well as a few others).
I would like to be able to put something like this in my docker-compose.yml file in order to handle these cases:
version: '3'
services:
my_fancy_library:
build: .
ports:
- "8080:8888"
environment:
- HCP_SUBJECTS_DIR="/hcp_subjects"
- SUBJECTS_DIR="/freesurfer_subjects"
volumes:
- "$SUBJECTS_DIR:/freesurfer_subjects"
- "$HCP_SUBJECTS_DIR:/hcp_subjects"
In testing this, if the user has both environment variables set, everything works swimmingly. However, if they don't have one of these set, I get an error about not mounting directories that are fewer than 2 characters long (which I interpret to be a complaint about mounting a volume specified by ":/hcp_subjects").
This question asks basically the same thing, and the answer points to here, which, if I'm understanding it right, basically explains how to have multiple docker-compose files that are resolved in some fashion. This isn't really a viable solution for my case for a few reasons:
This tool is designed for use by people who don't necessarily know anything about docker, docker-compose, or related utilities, so expecting them to write/edit their own docker-compose.yml file is a problem
There are more than just two of these directories (I have shown two as an example) and I can't realistically make a docker-compose file for every possible combination of these paths being declared or not declared
Honestly, this solution seems really clunky given that the information needed is right there in the variables that docker-compose is already reading.
The only decent solution I've been able to come up with is to ask the users to run a script ./run.sh instead of docker-compose up; the script examines the environment variables, writes out its own docker-compose.yml file with the appropriate volumes, and runs docker-compose up itself. This also seems somewhat clunky, but it works.
Does anyone know of a way to conditionally mount a set of volumes based on the state of the environment variables when docker-compose up is run?
You can set defaults for environment variable in a .env-file shipped alongside with a docker-compose.yml [1].
By setting your environment variables to /dev/null by default and then handling this case in the containerized application, you should be able to achieve what you need.
Example
$ tree -a
.
├── docker-compose.yml
├── Dockerfile
├── .env
└── run.sh
docker-compose.yml
version: "3"
services:
test:
build: .
environment:
- VOL_DST=${VOL_DST}
volumes:
- "${VOL_SRC}:${VOL_DST}"
Dockerfile
FROM alpine
COPY run.sh /run.sh
ENTRYPOINT ["/run.sh"]
.env
VOL_SRC=/dev/null
VOL_DST=/volume
run.sh
#!/usr/bin/env sh
set -euo pipefail
if [ ! -d ${VOL_DST} ]; then
echo "${VOL_DST} not mounted"
else
echo "${VOL_DST} mounted"
fi
Testing
Environment variable VOL_SRC not defined:
$ docker-compose up
Starting test_test_1 ... done
Attaching to test_test_1
test_1 | /volume not mounted
test_test_1 exited with code 0
Environment variable VOL_SRC defined:
$ VOL_SRC="./" docker-compose up
Recreating test_test_1 ... done
Attaching to test_test_1
test_1 | /volume mounted
[1] https://docs.docker.com/compose/environment-variables/#the-env-file
Even though #Ente's answer solves the problem, here is an alternative solution when you have more complex differences between environments.
Docker compose supports multiple docker-compose files for configuration overriding in different environments.
This is useful if you have different named volumes you need to potentially mount on the same path depending on the environment.
You can modify existing services or even add new ones, for instance:
# docker-compose.yml
version: '3.3'
services:
service-a:
image: "image-name"
volumes:
- type: volume
source: vprod
target: /data
ports:
- "80:8080"
volumes:
vprod:
vdev:
And then you have the override file to change the volume mapping:
# docker-compose.override.yml
services:
service-a:
volumes:
- type: volume
source: vdev
target: /data
When running docker-compose up -d both configurations will be merged with the override file taking precedence.
Docker compose picks up docker-compose.yml and docker-compose.override.yml by default, if you have more files, or files with different names, you need to specify them in order:
docker-compose -f docker-compose.yml -f docker-compose.custon.yml -f docker-compose.dev.yml up -d

Multiple docker .env for multiple servers(prod, dev) that containers use?

I really did not know how to word the title.
In my system I have two instances:
a Prod server
a Dev server
Dev used mostly for testing. In each case I have two versions of AMQP both having different hostnames.
To avoid duplication or unnecessary time rewriting the same code in multiple projects I wanted to use the env file that docker compose has, though everywhere I read, no one discusses this case. That case being that depending on where a stack is deployed is which env file it would use and that env file existing on the swarm itself rather than the individual projects.
Hopefully, I didn't miss anything when explaining this. Summary being two swarms each having their own env file that the containers deployed to it can use. Also if I need to reword anything, I will do so.
you can have multiple .env files and assign them to services in docker-compose.yml like
web:
env_file:
- web-variables.env
nginx:
env_file:
- nginx-variables.env
and if you want to change them for development environemnt you could override the docker-compose.yml with docker-compose.development.ymlfile and then start it with
docker-compose -f docker-compose.yml -f docker-compose.development.yml up -d

docker-compose scaleable way to provide environment variables

I am searching for a scaleable solution to the problem of having numerous possible environments for a containerised app. Let's say am creating a web app and I have the usual deployment environments, develop, testing, production but I also have multiple instances of the app for different clients, client1, client2, client3 etc.
This quickly becomes a mess if I have to create separate docker-compose files:
docker-compose-client1-develop.yml
docker-compose-client1-testing.yml
docker-compose-client1-production.yml
docker-compose-client2-develop.yml
...
Breaking the client specific configuration into a .env file and dockers variable substitution gets me most of the way there, I can now have one docker-compose.yml file and just do:
services:
webapp:
image: 'myimage:latest'
env_file:
- ./clients/${CLIENT}.env # client specific .env file
environment:
- DEPLOY # develop, testing, production
so now I just need the CLIENT and DEPLOY environment variables set when I run docker-compose up which is fine, but I'm wondering about a convenient way to pass those environment variables in to docker-compose. There's the potential (at least during development) for a decent amount of context-switching. Is there a tidy way to pass in different CLIENT and DEPLOY env vars to docker-compose up every time I run it?
What you are trying to achieve is to set environment variables per-command.
Are you running on Linux? Take a look at env command. Just prepend your docker-compose command line like this:
env CLIENT=client1 DEPLOY=production docker-compose ...
On Windows, you may have to do something more complicated (like this), but there could be simpler ways.
Have you tried docker-compose file extending?
For instance you can have base docker-compose.yml file which is the production one and multiple extending files where you only change what needs to be overloaded:
docker-compose.dev.yml
version: '2'
services:
webapp:
env_file: path_to_the_file_env
Then you simply use both:
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
To spin up the production it's as easy as:
docker-compose up
I personally use this technique a lot in many of my projects.

How to conditionally run different configs in docker-compose.yml?

I have configured SSL via certbot on live server. I have a volume mapping for this in nginx section of docker-compose.yml:
volumes:
...
- /etc/letsencrypt:/etc/letsencrypt
This works just fine on the live server but I have a different setup on my local machine, where I run the app and see it on http://localhost. I suppose I don't need SSL on my local machine, so probably I just can exclude this part of setup if it runs locally.
Also this case makes me think I will have to potentially configure some other things differently locally vs live.
So, the question is how to properly distinguish these differences between local and live setups and apply them (semi)automatically depending on the environment?
Environment variables are typically a good way to create simple runtime portability like this, and many tools/apps/packages support runtime configuration by nothing more than environment variables. Unfortunately, nginx is not one of those apps
For nginx, try something like this:
environment:
- NGINX_CONF=localhost.conf
Here, localhost.conf would be your nginx configuration for your local machine. Run an entrypoint.sh of some kind, and symlink the config specified by NGINX_CONF to wherever nginx will pick it up (usually /etc/nginx/conf.d or /etc/nginx/sites-enabled).
ln -s /etc/nginx/conf.d/running.conf /app/nginx-confs/${NGINX_CONF}
This assumes you have all of your confs copied to the container in /app/nginx-confs, but they can live wherever you like. The localhost.conf would serve your site as http://localhost.
For your live server, pass NGINX_CONF=liveserver.conf. This conf would serve https://www.liveserver.com or whatever you host is.
At this point, you can choose which nginx configuration you'll run when you start your container, using environment variables. Even if you don't want to do it this way, hopefully it gets you moving in the right direction and thinking about environment variables as a way to configure at runtime.
There are other, more granular ways of managing nginx confs at runtime. Something like confd or a templating engine like the mustache templating engine are options. There are roundabout ways in nginx with env directive and set_by_lua, but this feels like the most hacky solution to me so I prefer the others.
You can write a Makefile to generate different docker-compose.yml files depending on your needs.
Makefile:
# Makefile
-include config.mk
A_CONFIG_VAR ?= default_value
all: docker-compose.yml
docker-compose.yml: docker-compose.yml.in config.mk
#echo 'Generating docker-compose.yml'
#sed -e 's|##A_CONFIG_VAR##|$(A_CONFIG_VAR)|g' $< > $#
config.mk: put your configuration variables in this file.
# config.mk
A_CONFIG_VAR = "a_value"
docker-compose.yml.in: write an input docker-compose.yml file like so
volumes:
- /path/to/somewhere:##A_CONFIG_VAR##
Change the contents of config.mk to suit your needs. And then run
make
This should generate a docker-compose.yml file for you.

Resources