"Unable to read key" Laravel Error Deploying to Dokku/Heroku

I've recently had all sorts of issues building a Laravel app for Dokku because the Heroku buildpack it uses builds the application in an isolated container that has no access to the storage/ directory. I consistently found myself getting errors such as the following when running artisan commands:

Unable to read key from file file:///tmp/build/storage/oauth-public.key {"exception":"[object] (LogicException(code: 0): Unable to read key from file file:///tmp/build/storage/oauth-public.key at /tmp/build/vendor/league/oauth2-server/src/CryptKey.php:64)
In CryptKey.php line 64:
Unable to read key from file file:///tmp/build/storage/oauth-public.key

The main issue is that it was in a state where all artisan commands would fail, including php artisan passport:keys, which would usually be used to generate these keys.

My solution is to create valid keys using the composer scripts, that at least allow your build to pass. After struggling a lot with the correct format, it seems the keys need to be generated by openssl in the PEM format, with RSA encryption. Plain old ssh-keygen won't work here. Here's the command to generate the keys:

openssl genpkey -algorithm RSA -out ./storage/oauth-private.key -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in ./storage/oauth-private.key -out ./storage/oauth-public.key

To actually make that effective, you need to add it to your composer.json scripts. I use the "compile" scripts, as it's used by Heroku and Dokku, and isn't a standard composer scripts section.

Here's how your "scripts" section might look:

{
    // ...
    "scripts": {
        "post-autoload-dump": [
            "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
            "if test \"$(pwd)\" != \"/tmp/build\"; then php artisan package:discover --ansi; fi"
        ],
        "post-root-package-install": [
            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
        ],
        "post-create-project-cmd": [
            "@php artisan key:generate --ansi"
        ],
        "post-install-cmd": [
            "Illuminate\\Foundation\\ComposerScripts::postInstall",
            "if test \"$(pwd)\" != \"/tmp/build\"; then php artisan optimize; fi"
        ],
        "post-update-cmd": [
            "Illuminate\\Foundation\\ComposerScripts::postUpdate",
            "if test \"$(pwd)\" != \"/tmp/build\"; then php artisan optimize; fi"
        ],
        "compile": [
            "if test ! -f 'storage/oauth-public.key'; then openssl genpkey -algorithm RSA -out ./storage/oauth-private.key -pkeyopt rsa_keygen_bits:2048 && openssl rsa -pubout -in ./storage/oauth-private.key -out ./storage/oauth-public.key; fi",
            "@php artisan clear-compiled",
            "@php artisan optimize",
            "@php artisan migrate --force",
            "@php artisan config:clear",
            "@php artisan cache:clear"
        ]
    }

I've highlighted the relevant line in bold, note it comes before any php artisan commands.

There's an extra tidbit there too: the checks before running php artisan optimize in the "post-install-cmd" and "post-update-cmd scripts" sections. This checks if we're running the code in the /tmp/build, which would indicate we're in a Heroku build environment, and ensures we don't run code intended for development/CI environments. In these cases, the php artisan optimize command would throw the same error.

This is obviously quite a hacky approach, but I couldn't find a better solution.

Posted on Jan 10, 2024

Discuss This

blog comments powered by Disqus