I usually expose ports like `127.0.0.1:1234:1234` instead of `1234:1234`. As far as I understand, it still punches holes this way but to access the container, an attacker would need to get a packet routed to the host with a spoofed IP SRC set to `127.0.0.1`. All other solutions that are better seem to be much more involved.
If you have your own firewall rules, docker just writes its own around them.
$ nc 127.0.0.1 5432 && echo success || echo no success no success
Example snippet from docker-compose:
DB/cache (e.g. Postgres & Redis, in this example Postgres):
[..]
ports:
- "5432:5432"
networks:
- backend
[..]
App: [..]
networks:
- backend
- frontend
[..]
networks:
frontend:
external: true
backend:
internal: true