There are two general approaches. The first is simpler, the second makes it easier to allow future charms to build off of your work.
First of all - a charm is a set of scripts that manages an application’s lifecycle. But that “application” might be a full service. Those scripts have full power to do whatever they want. So if you want to deploy multiple applications that work together to create a single service, that’s perfectly fine.
There are some layers, such as nginx, that make it easy to place your web application server behind NGINX without further configuration. NGINX won’t be visible in the Juju status output though.
Another approach is to use a bundle and interfaces. A bundle is a description of several charms that work together, written in declarative YAML format. Interfaces allow charms to say things like: “I provide a web server”, “I need to connect a PostgreSQL database”, “I need to talk to Redis”, “I provide PostgreSQL”*, etc.
Using bundles and interfaces is more complex, but has a steeper learning curve. I suggest doing everything in a single charm, then extending it out into multiple charms as your knowledge grows.
* This might not not actually be PostgreSQL, it might be a charm that looks to clients like PostgreSQL, such as pgbouncer.