Combining CD and GitFlow
Conflating deployment environments and branches often leads to problems like you describe.  It does seem like a natural fit though and easily maps to mental model of an ideal solution but, real life is anything but ideal.

Basically, git is not a deployment tool, one should not clone to deploy a version but fetch a package (a tarball, zip file, docker container, NPM package etc).

In practice these two concepts are orthogonal.  Branching strategies (whichever it may be, gitflow, trunk based etc) is a means to assist developers isolating and sharing their work.  It ties naturally to the versioning strategy and is what forms the CI half of your CI/CD system.  Here you must force yourself to keep CI (continuous integration) separate from CD (continuous deployment) as you will see if you read-on. 

Deployment however consumes the outcome of versions that were created and integrates various parts of the system and makes a whole that is usable, testable.

So if you view your branches and the code therein as a means to assemble a version which the build system will build and package and then store it somewhere.  Can be as simple as a network share or FTP site or it could tie in to an existing package manager with a appropriate back end, whatever suits your team best really.  This choice will depends greatly on the technology stack you are using and the underlying platforms you will be deploying to (static environments, docker, kubernetes, standalone copy-and-run etc).

Then another set of tasks can pull in any of these versions and assemble a working deployment.  Here this works best if your deployment environment are dynamic but it can be made to work with static environments as well.  This will form the CD part of your continuous system.

Once decoupled you can spin up an environment that would contain any versions that has been previously built.

In term you could even have multiple production environments with slight variations of a user interface tweaks and pit them against each-other with techniques like A/B testing and clever load balancing / distribution to end-users.

Going back to your particular situation it will simplify your gitflow back to a more traditional master/develop permanent branches and a set of features/hotfix and release branches.

Using semver you can keep official versions strictly for master merges (from release branches or hotfix branches) and pre-release versions for all other branches.  for your pre-release branches simply add a -SOMETHING where something is the name of the branch for example and possibly a timestamp to differentiate successive deployments.

for example, 


release branches would get the x.y.0-RCi (i th release candidate). x.y is the version that will be released where the -RC is simply dropped when merged in master.  0 as the patch because release branches are used for new features and product evolution
hotfix branch follow the same principle as the release branch except the patch number is set to a non-zero value.
development branch could get x.y.z-dev.timestamp
feature branch x.y.z-JiraID-timestamp.  Where JiraID is the ticket number of the system you use to track new features (like Jira, Bugzilla, etc)


I propose semantic versioning here because I am familiar with it but the same broad principle of segregating branches through versioning can be applied with pretty much any versioning scheme.

Nowhere in the source control do you see anything that relates to deployment environment.  Only your versioning strategy is what links them, they are otherwise completely isolated and independent.

Your devs will feel more comfortable in this new environment and your deployments will feel less constrained and awkward.

Hope this helps