Docker alone doesn't solve the problem and neither does pip unless you take extra steps.
Here's a common use case to demonstrate the issue:
I open source a web app written in Flask and push it to GitHub today with a requirements.txt file that only has top level dependencies (such as Flask, SQLAlchemy, etc.) included, all pinned down to their exact patch version.
You come in 3 months from now and clone the project and run docker-compose build.
At this point in time you're going to get different versions than I had 3 months ago for many sub-dependencies. This could result in broken builds. This happened multiple times with Celery and its sub-dependency of Vine and Flask with its sub-dependency of Werkzeug.
So the answer is simple right, just pip freeze your requirements.txt file. That works but now you have 100 dependencies in this file when really only about 8 of them are top level dependencies. It becomes a nightmare to maintain that as a human. You basically need to become a human dependency resolution machine that traces every dependency to each dependency.
Fortunately pip has an answer to this with the -c flag but for such a big problem it's not very well documented or talked about.
It is a solvable problem tho, to have a separate lock file with pip without using any external tools and the solution works with and without Docker. I have an example of it in this Docker Flask example repo https://github.com/nickjj/docker-flask-example#updating-depe..., but it'll work without Docker too.
Also, Docker is not a universal and secure solution. It works great as an "universal server executable format and/or programming environment" on Linux, but less so on Windows, macOS and especially FreeBSD.
Yes at some level it solves your problem, but it adds a lot of complexity which doesn't need to exist. Also now depend on someone in the middle who takes effort to manage, and might not do exactly what you want.
If you are developing an open source software that people can install on their machines, is a terrible solution. In that case you should package it correctly and distribute it via pip, so people can easily install it on their systems.
===
[Marine has been wounded]
Marine Private: AHHHH my arm, my arm!
Major Payne: Want me to show you a little trick to take your mind off that arm?
[Marine nods and Payne grabs the private's pinky finger]
Major Payne: Now you might feel a little pressure.
[Major Payne breaks the Marine's pinky]
Marine Private: AUGGGGH! My finger, my finger!
Major Payne: Works every time.
====
That's kind of how I feel about Docker. Before, you had a problem. With Docker, you have a new, bigger problem (and most of your old problem hasn't gone away; it's just been masked for a while).
(And yes, I know I'm in the minority here)
* Robust systems shouldn't be tied to pinned versions. If your code works with PostgreSQL 9.6.19, and doesn't work with 9.6.20 or 9.6.18, that's usually the sign of something going very, very wrong.
* In particular, robust systems should always work with the latest versions of libraries. In most cases, they should work with stock versions of libraries too (whatever comes with Ubuntu LTS, Fedora, or similar). It's okay if you have one or two dependencies in a system beyond that, but if it's a messy web, that's a sign of something going very, very wrong.
* Even if that's not happening, as much as I appreciate having decoupled, independent teams, your whole system should work with the same versions of tools and libraries. If one microservice only works with PostgreSQL 11.10, and another with 12.07, that's a sign of something having gone way off the rails.
These aren't hard-and-fast rules -- exceptional circumstances come up (e.g. if you're porting Python 2->Python 3, everything might not land at the same time) -- but these should be rare enough to be individually approved (and usually NOT approved) by your chief architect/architecture council/CTO/however you structure this thing.
For the most part, I've seen Docker act as an enabler of bad practices:
* Each developer can have an identical install, so version dependencies creep in
* Each team has their own container, and it's easy for versions and technologies to diverge
* With per-team setups, you end up with an uncontrollable security perimeter, since you need to apply patches to a half-dozen different versions of the same library (or worse, libraries performing the same function)
The docker/microservices/etc. mode of operating gives a huge short-term productivity boost, but I haven't actually seen a case on teams I've been on where the benefits outweigh the long-term costs. That's not to say they don't exist, but they're in the minority.
For the most part, I use Python virtual environments and similar, but by the time you hit docker, I back away.