Obviously a good firewall (ufw suffices) is a must. Using a reverse proxy web server back to your web apps (I prefer nginx, but caddy is also another good one). In your reverse proxy web server, also setting up web application firewall config to flag suspicious things (for example, anyone going to a URL with `../` or `/etc/passwd` is clearly up to no good, as is a user-agent that's a known scraping tool). In particular, I like using the custom HTTP 444 response code with nginx, so I can instantly flag the worst offenders.
Then, use fail2ban to blacklist hosts that are up to no good. If you can create a regex for something in a logfile, you can automatically ban almost anything. However, fail2ban is a very powerful footgun, and if misconfigured you can easily ban yourself from your own server! However, fail2ban is probably the best hardening tool, since you greatly reduce the number of tries that someone has to exploit something, and severely slow them down.
Finally, regular monitoring and patching. I swear by check_mk for monitoring, so I can see every suspicious query that's coming through, and instantly identify most ongoing attacks. Fail2ban takes care of 99% of the work if it's configured correctly, but the worst attacks are the ones that you don't know are happening.
I've been self-hosting most of the webapps I depend on for almost 10 years now, and I can say that self-hosting is extremely fun, but does require a a decent time commitment to maintain your infrastructure. However, if you are willing to invest time in automation with some basic shell scripting, you can get this down to less than an hour per week, which is mainly just checking your monitoring console and scheduled jobs.
If this sounds too daunting but you still want to go this route, check out yunohost: https://yunohost.org/#/