4

Suppose I wrote a docker-compose.dev.yml file to set the development environment of a Flask project (web application) using Docker. In docker-compose.dev.yml I have set up two services, one for the database and one to run the Flask application in debug mode (which allows me to do hot changes without having to recreate/restart the containers). This allows everyone on the development team to use the same development environment very easily. However, there is a problem: it is evident that while developing an application it is necessary to install libraries, as well as to list them in the requirements.txt file (in the case of Python). For this I only see two alternatives using a Docker development environment:

  1. Enter the console of the container where the Flask application is running and use the pip install ... and pip freeze > requirements.txt commands.
  2. Manually write the dependencies to the requirements.txt file and rebuild the containers.

The first option is a bit laborious, while the second is a bit "dirty". Is there any more suitable option than the two mentioned alternatives?

Edit: I don't know if I'm asking something that doesn't make sense, but I'd appreciate if someone could give me some guidance on what I'm trying to accomplish.

Tedpac
  • 862
  • 6
  • 14
  • 1
    How would you manage these dependencies without Docker involved? Can you do day-to-day development in a non-Docker Python virtual environment, and track the `setup.cfg` and `requirements.txt` files in source control with the rest of your application? – David Maze May 14 '22 at 10:35
  • Why not have another file: `requirements_dev.txt` which, as its first line, has: `-r requirement.txt` in it. However, a better solution would be using a dependency/package management tool like `poetry` or `pip-tools` - For what it's worth,I personally favour `pip-tools`. On the other hand, `poetry` has a more straight-froward way to separate production and dev/test dependencies. – rocksteady May 21 '22 at 10:12

4 Answers4

5

For something like this I use multi-layer docker images.

Disclaimer: The below examples are not tested. Please consider it as a mere description written in pseudo code ;)

As a very simple example, this approach could look like this:

# Make sure all layers are based on the same python version.
FROM python:3.10-slim-buster as base

# The actual dev/test image.
# This is where you can install additional dev/test requirements.
FROM base as test
COPY ./requirements_test.txt /code/requirements_test.txt
RUN python -m pip install --no-cache-dir --upgrade -r /code/requirements_test.txt

ENTRYPOINT ["python"]
# Assuming you run tests using pytest.
CMD ["-m", "pytest", "..."]

# The actual production image.
FROM base as runtime
COPY ./requirements.txt /code/requirements.txt
RUN python -m pip install --no-cache-dir --upgrade -r /code/requirements.txt

ENTRYPOINT ["python"]
# Assuming you wantto run main.py as a script.
CMD ["/path/to/main.py"]

With requirements.txt like this (just an example):

requests

With requirements_test.txt like this (just an example):

-r requirements.txt

pytest

In your docker-compose.yml file you only need topassthe --target (of the multi-layered Dockerfile, in this example: test and runtime) like this (not complete):

services:
  service:
    build:
      context: .
      dockerfile: ./Dockerfile
      target: runtime  # or test for running tests

A final thought: As I mentioned in my comment, a much better approach for dealing with such dependency requirements might be using tools like poetry or pip-tools - or whatever else is out there.


Update 2022-05-23:

As mentioned in the comment, for the sake of completeness and because this approach might be close to a possible solution (as requested in the question):

An example for a fire-and-forget approach could look like this - assuming the container has a specific name (<containe_name>):

# This requires to mount the file 'requirements_dev.txt' into the container - as a volume.
docker exec -it <container_name> python -m pip install --upgrade -r requirements_dev.txt

This command simply installs new dependencies into the running container.

rocksteady
  • 2,320
  • 5
  • 24
  • 40
  • The `base` layer of the Dockerfile could even already install system or python dependencies common to both environments (targets) (`runtime` and `test`). – rocksteady May 21 '22 at 10:38
  • It's an interesting way to manage different environments of an application, also thanks for mentioning `poetry`, but we're back to the same question: how does a developer manage new Python dependencies that arise during development? It is worth noting again my main goal: to eliminate the need for other developers in the team to have Python or the database installed locally. – Tedpac May 23 '22 at 02:33
  • Using docker eliminates the need for other developers to install python or a database - I don't see a problem here. I guess this is why you opted for docker. Regarding the introduction of new dependencies while developing: In my opinion, this is what a development environment is used for andwhat I can expect from developers - Changing or adding a dependency in a `requirements_test.txt` file locally and simply building/starting the image/container again. The result will contain the changes. If this should be added to version control or reverted before pushing is yet another question. – rocksteady May 23 '22 at 06:15
  • 1
    An example for a fire-and-forget approach could look like this - assuming the container has a specific name (``): ``` # This requires to mount the file 'requirements_dev.txt' into the container - as a volume. docker exec -it pyton -m pip install --upgrade -r requirements_dev.txt ``` This command simply installs new dependencies into the running container. – rocksteady May 23 '22 at 08:20
  • What you just mentioned in the last comment is the closest to what I've been looking for. That approach allows me to install a library with a single command, and update the requirements.txt file also with a single command, just as one would do with a local Python **virtualenv/venv**, plus the requirements.txt changes will be reflected locally, so I can upload them to a repository. Could you include that information in your answer please? – Tedpac May 23 '22 at 17:19
  • The only other idea I would offer is mounting a directory on the host system to the container and perform the copy directive. Then the user can use pip to dump the installed packages to a text file in the shared directory. Docker containers are intended to be ephemeral. The practice I'm describing is more inline with this idea. – VanBantam May 23 '22 at 18:28
1

If the goal is to have a consistent dev environment, the safest way I can think of would be to build a base image with the updated dependencies and publish to a private registry so that you can refer to a specific tag like app:v1.2. So the Dockerfile can look like:

FROM AppBase:v1.2
...

This means that there is no need to install the dependencies and results in a quicker and consistent dev env setup.

Anwar Husain
  • 1,414
  • 14
  • 19
  • It's an interesting idea, but once a developer downloads the image (e.g. `AppBase:v.1.2`) and creates a container, we're back to the same question: how does that developer manage new Python dependencies that arise during development? – Tedpac May 23 '22 at 01:48
  • 1
    I see, sorry, I did assume that the rate of adding the packages was not that often and I focused on the aspect of `reproducible` envs. If the container is used as a dev env, then I would probably go with option 1 because it's safe but use a package manager (so it's not very laborious) like `poetry`. So the developer would add the package via `poetry add ` which would add it in the `.toml` file. You can then use the `poetry.lock` file in a pipeline to publish a base image for reproducibility may be? – Anwar Husain May 23 '22 at 09:30
1

Install requirements in a virtualenv inside the container in an externally mounted volume. Note that the virtualenv creation and installation should happen in container run time, NOT in image building time (because there is no mounted volume).

Assuming you are already mounting (not copying!) your project sources, you can keep it in a ./.venv folder, which is a rather standard procedure.

Then you work just as you would locally: issue the install once when setting up the project for the first time, requirements need not be reinstalled unless requirements change, you can keep the venv even if the container is rebuilt, restarting the app does not reinstall the requirements every time, etc, etc.

Just don't exepect the virtualenv to be usable outside the container, e.g. by your IDE (but a bit of hacking with the site module would let you share the site-packages with a virtualenv for your machine)


This is a very different approach to how requirements are usually managed in production docker images, where sources and requirements are copied and installed in image building time. So you'll probably need two very different Dockerfiles for production deployment and for local development, just as you already have different docker-compose.yml files.

But, if you wanted them both to be more similar, remember there is no harm on also using a virtualenv inside the production docker image, despite the trend of not doing so.

N1ngu
  • 2,862
  • 17
  • 35
  • I don't know if I misunderstood, but what I want to avoid is having the need to create a **virtualenv** on my local machine, because that would also force other developers to have to install the specific version of Python on their machines. – Tedpac May 23 '22 at 02:21
  • The virtualenv should be created inside the container! Just in a volume mounted externally. With this technique all developers can use the specific python interpreter from the container. – N1ngu May 23 '22 at 10:05
  • Oh, I see. Now, how will developers go about using that **virtualenv** that it's inside the container? Is there a way that they can use the **virtualenv** without having a Python interpreter installed on their local machines? – Tedpac May 23 '22 at 17:01
  • The virtualenv should be used exclusively from inside the container, replacing all interpreter references to the mounted /my/app/path/.venv/bin/python. For this purpose, managing the venv with pipenv or poetry makes your life much easier thanks to the `pipenv|poetry run` subcommands. E.g. to start the interpreter: `docker-compose run my-app pipenv run python` – N1ngu May 24 '22 at 13:35
0

The second option is generally used in python environments. You just add new packages to requirements.txt and restart the container, which has a line with pip install -r requirements.txt in its dockerfile that do the installing.

Javad Zahiri
  • 13
  • 2
  • 3