Using per-user secrets - stored in either the database or the file system, or indeed anywhere else - completely misses the point of JWTs. Well, one of the points, and using symmetric secrets (rather than public keys) means you don't get the other, either.
If you have to look up a value before you can even verify a JWT, you've destroyed the scalability and statelessness of the format. Sure, in practice you'll store lots of info in the DB, much of it per-user, that you'll be requesting all the time anyway... but if you're going to make your session management system depending on those kinds of lookups then you should really consider just using random blobs for your session tokens. Same lookup hit, less compute time after retrieving the secret to verify it, and you can actually revoke them without complicated shenanigans.
To answer your actual question, the ideal setup is an HSM of some sort (such that actually extracting the keys is just not possible), but lacking that the usual approaches are:
A. Stored in a KMS (Key Management Service) that the server can talk to; server retrieves the secret(s) at startup / periodically and stores it/them in RAM. Of course, proving identity to the KMS often adds another layer of "where do you store secrets?"; the main advantages of this scheme are a central location to manage keys for a whole cluster, and the ability to keep the keys off of any persistent storage outside one particular system.
B. Stored in an environment variable or command-line arg passed to the server. Obviously the process that invokes the server needs to know the secret too, but this can be combined with the previous approach to have no persistent storage on the server host but also allow creating arbitrarily many short-lived instances of the server without each one pinging the KMS. Alternatively, supports starting the server process via remote command, with the secret not resident anywhere on the server host (even in memory) until the server starts.
C. Stored in a file outside the webroot, but somewhere the server has access to (your #1). Ideally, the location should not be somewhere the server has persistent access to, though, either because it drops privileges after reading in the secret, or because the secret is read through a named pipe or something and not available on demand.
For simple systems like you're probably envisioning, where quick development and low cost matters more than security in the extreme edge cases, you probably want to go with approach #1 / C, if you use JWTs at all.
Speaking of extreme edge case concerns, brute-forcing any reasonable length (typically cited as 128 bits i.e. 16 bytes) of cryptographically random data (which PHP's
random_bytes provides) is not remotely a concern. With a 256-bit (32-byte) value, assuming Moore's Law continues on track (which it hasn't quite, lately), the entire world's compute capability, working together, might be able to brute-force a single secret... if you give them three to four centuries to do it. You probably won't care by then.