One reason the accepted solution works for some people but not for others lies in a design issue of firewalld. Firewalld only processes the first matching zone for any connections. It also processes zones based on IP address before zones based on interfaces. As long as any of your zones happens to include the IP addresses docker is using, the ACCEPT rule in the trusted zone will never get processed.
To verify this, you can look at the generated iptables rules unless you are using the nbt backend:
iptables -vnL | less
The solution is to use a firewalld direct rule instead of the trusted zone. Direct rules are always processed first. For example, for port 3306 (i.e., mysqld on the local host), you'd need this rule:
/bin/firewall-cmd --direct --permanent --add-rule ipv4 filter INPUT 10 -p tcp --dport 3306 -i docker0 -j ACCEPT
If you repeat the same command without the --permanent switch, firewalld will make the change on the fly for you, and you should not need to restart docker.
A second reason is that, depending on the version of docker and the networking you configured, traffic may not actually flow across the docker0 interface, but through a separate bridge interface. You can see that interface with the command
ip address show
You will have to repeat the direct-rule statement above with this bridge interface.
You can also specify your own network in docker. This post describes the process: https://serverfault.com/questions/885470/how-to-configure-firewall-on-user-defined-docker-bridge
The short version is that you specify the networks section in a docker-file, like this:
version: '3'
.
.
.
networks:
mynetwork:
driver_opts:
com.docker.network.bridge.name: mynetwork_name
You can also do this on the command line.