Traditionally, software has been created to be run on a physical machine. But we quickly hit a wall when we do this. Software can only run on certain hardware, for example, it requires a certain processor.
Furthermore, more complex software usually does not run completely autonomously, but is integrated into a software ecosystem. This includes an operating system, libraries, and dependencies. The right versions of all the components must be available for them to interact correctly. There is also a configuration, which describes how the individual components are linked to each other.
If you want to run several applications on one machine in parallel, version conflicts quickly arise. An application may require a version of a component that is incompatible with another application. In the worst-case scenario, each application would have to run on its own physical machine. What is true is that physical machines are expensive and cannot be scaled easily. So if an application’s resource requirements grow, it may need to be migrated to a new physical machine.
Another problem arises from the fact that software under development is used in different environments. A developer writes code on the local system and runs it there for testing. The application goes through several test stages before going into production, including a test environment for quality assurance or a staging environment for testing by the product team.