I’ll see about digging up recommendations if I can, but I’m on my phone right now.
My biggest single piece of advice would be this: Understand that your reader does not share your context.
What this means is that you have to question your assumptions. Ask yourself, is this something everyone knows, or something only I know? Is this something that’s an accepted standard, or is it simply my personal default? If it is an accepted standard, how widely can I assume that accepted standard is known?
A really common example of this in self-hosting is poorly documented Docker instructions. A lot of projects suffer from either a lack of instructions for Docker deployment, because they assume that anyone deploying the project has spent 200 hours learning the specifics of chroot and namespaces and can build their own OCI runtime from scratch, or needlessly precise Docker instructions built around one hyper-specific deployment method that completely break when you try to use them in a slightly different context.
A particularly important element of this is explaining the choices you’re making as you make them. For example a lot of self-hosted projects will include a compose file, but will refuse to in any way discuss what elements are required, and what elements are customisable. Someone who knows enough about Docker, and has lots of other detailed knowledge about the Linux file system, networking, etc, can generally puzzle it out for themselves, but most people aren’t going to be coming in with that kind of knowledge. The problem is that programmers do have that knowledge, and as the Xkcd comic says, even when they try to compensate for it they still vastly overestimate how much everyone else knows.
OK, I said I’d try for examples later, but while writing this one did come to mind. Haugene’s transmission-openvpn container implementation has absolutely incredible documentation. Like, this is top tier, absolutely how to do it; https://haugene.github.io/docker-transmission-openvpn/
Starts off with a section that every doc should include; what this does and how it does it. Then goes into specific steps, with, wonder of wonders, notes on what assumptions they’ve made and what things you might want to change. And then, most importantly, detailed instructions on every single configuration option, what it does, and how to set it correctly, including a written example for every single option. Absolutely beautiful. Making docs like this is more work, for sure, but it makes your project - even something like this that’s just implementing other people’s apps in a container - a thousand times more usable.
(I’ve focused on docker in all my examples here, but all of this applies to non-containerized apps too)
That really is a good piece of documentation.