Configuring Craft CMS with Redis for use on Heroku
This post is about connecting a Heroku app built in Craft CMS to Heroku Redis with an SSL connection.
I’ve recently taken an app from the free tier to a paid tier on Heroku and was presented with a problem with connecting my Craft CMS app to Redis. I hope this post is helpful to someone else who’s found themselves in this situation.
Upgrading Heroku Redis from the free tier to a paid tier requires that you connect to Redis through a secure connection. This difference is why your “free” app might work but your “paid” app will appear broken. Looking through the Github issues on this topic, you may find this post, Doesn’t work with Heroku Redis (TLS required, Redis 6+). The solution mentioned in one of the comments turned out to be the solution. In short, include this in your config.
'useSSL' => true,
'contextOptions' => [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
],
This suggestion does work. You could probably configure the app to have the SSL option validate the peer and peer name, but that’s beyond the scope of what I’m writing about today though.
Why is there any more to this post?
If that’s the solution, you might wonder why this post exists. Fair question!
Even though I had it configured to work and it was working, I was under the impression it was incorrect. I would log into the server via the command line with heroku ps:exec
and then try to run the ./craft
command. It reported that “the redis database is not reachable as configured.”
I rewrote my app.php
many times. Locally, I was able to connect to the Heroku Redis but when I was on the Heroku server, I would always receive this error. To my surprise, just looking at the app in my browser, everything seemed fine. It’s just that this error message on the command line seemed like something I needed to fix.
The lightbulb moment for me was when I removed Redis from my app entirely. My expectation was that since Craft would then rely on its own database for storing cache requests, the command line ./craft
command would work. The surprise is that it didn’t.
Removing Redis produced a different error message:
The database is not reachable as configured.
How can that be? I can see my Craft website is accessing the database because I can see the entries. Why would the ./craft
command report that it can’t access the database?
Looking through the Heroku docs
I often think of using the command line on a Heroku server as “SSH-ing into the server.” This terminology is not entirely accurate though. You can’t simply use SSH to log into a Heroku server. You use the heroku-cli and the Heroku Exec command, as described in the documentation.
Heroku Exec is a feature for creating secure TCP and SSH tunnels into a dyno. It allows for SSH sessions, port forwarding, remote debugging, and inspection with popular Java diagnostic tools.
Near the end of the documentation, there is a section on limitations of the ps:exec
command.
Specifically, there are limitations when it comes to environmental variables.
The SSH session created by Heroku Exec will not have the config vars set as environment variables (i.e., env in a session will not list config vars set by heroku config:set).
This is the key.
Let’s review the situation again. The Craft command-line tool reported it couldn’t access the Redis database even when it appeared to be working. Then, after removing Redis from Craft entirely, the command-line tool then reported that it couldn’t access the database itself, even though it obviously could when looking at the site in a browser.
The Redis connection problem and the database connection problems are not due to a misconfigured Craft app. The problem is that the ./craft
command, when run from Heroku using the ps:exec
command does not have access to any of the environmental variables we typically expect to have access to.
Without any credentials, the ./craft
command can’t do anything to any database. I don’t know that it is possible to run any ./craft
command from the command line when using Heroku. That’s a bummer, but at least I’ve got Redis working on my app. ¯\_(ツ)_/¯
Heroku Dashboard
I’ll wrap up with a screenshot of how my app is configured in the Heroku Dashboard. You can see I have a web dyno, configured to 2x, but use what works for you. I have a worker. I have a ClearDB MySQL database and I have Heroku Redis.
Procfile
My Procfile is where the web dyno and worker dynos were created.
web: vendor/bin/heroku-php-apache2 web
worker: ./craft queue/listen --verbose
You’ll need to also upgrade the worker after you create it to have at least 1 dyno. That is what you see in the screenshot above.
app.php & app.web.php
It may be helpful to you to see my modified app.php
and app.web.php
files. These are modified versions of the app.php and app.web.php file from Andrew Welch’s Craft configuration.
app.php
<?php
/**
* Yii Application Config
*
* Edit this file at your own risk!
*
* The array returned by this file will get merged with
* vendor/craftcms/cms/src/config/app.php and app.[web|console].php, when
* Craft's bootstrap script defines the configuration for the entire
* application.
*
* You can define custom modules and system components, and even override the
* built-in system components.
*
* If you want to modify the application config for *only* web requests or
* *only* console requests, create an app.web.php or app.console.php file in
* your config/ folder, alongside this one.
*/
use craft\helpers\App;
$redisUrl = App::env('REDIS_URL');
if (!$redisUrl) {
exit('app.php: The redis database is not reachable as configured:' . App::env('REDIS_URL'));
}
$redisSettings = [
'id' => App::env('APP_ID') ?: 'CraftCMS',
'modules' => [
'site-module' => [
'class' => \modules\sitemodule\SiteModule::class,
],
],
'bootstrap' => ['site-module'],
'components' => [
'cache' => [
'class' => yii\redis\Cache::class,
'defaultDuration' => 86400,
'keyPrefix' => App::env('APP_ID') ?: 'CraftCMS',
'redis' => [
'hostname' => parse_url(App::env('REDIS_URL'), PHP_URL_HOST),
'port' => parse_url(App::env('REDIS_URL'), PHP_URL_PORT),
'database' => App::env('REDIS_CRAFT_DB'),
'password' => App::env('REDIS_USE_SSL') ? parse_url(App::env('REDIS_URL'), PHP_URL_PASS) : null,
'useSSL' => App::env('REDIS_USE_SSL'),
'contextOptions' => [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
],
],
],
'deprecator' => [
'throwExceptions' => App::env('DEV_MODE'),
],
'queue' => [
'class' => craft\queue\Queue::class,
'ttr' => 10 * 60,
],
'redis' => [
'class' => yii\redis\Connection::class,
'hostname' => parse_url(App::env('REDIS_URL'), PHP_URL_HOST),
'port' => parse_url(App::env('REDIS_URL'), PHP_URL_PORT),
'database' => App::env('REDIS_DEFAULT_DB'),
'user' => App::env('REDIS_USE_SSL') ? parse_url(App::env('REDIS_URL'), PHP_URL_USER) : null,
'password' => App::env('REDIS_USE_SSL') ? parse_url(App::env('REDIS_URL'), PHP_URL_PASS) : null,
'useSSL' => App::env('REDIS_USE_SSL'),
'contextOptions' => [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false
],
],
],
],
];
return $redisSettings;
app.web.php
<?php
/**
* Yii Web Application Config
*
* Edit this file at your own risk!
*
* The array returned by this file will get merged with
* vendor/craftcms/cms/src/config/app.php and app.[web|console].php, when
* Craft's bootstrap script defines the configuration for the entire
* application.
*
* You can define custom modules and system components, and even override the
* built-in system components.
*
* This application config is applied only for *only* web requests
*/
use craft\helpers\App;
return [
'components' => [
'session' => static function() {
// Get the default component config
$config = App::sessionConfig();
// Override the class to use Redis' session class and our config settings
$config['class'] = yii\redis\Session::class;
$config['keyPrefix'] = App::env('APP_ID') ?: 'CraftCMS';
$config['redis'] = [
'hostname' => parse_url(App::env('REDIS_URL'), PHP_URL_HOST),
'port' => parse_url(App::env('REDIS_URL'), PHP_URL_PORT),
'database' => App::env('REDIS_DEFAULT_DB'),
'password' => App::env('REDIS_USE_SSL') ? parse_url(App::env('REDIS_URL'), PHP_URL_PASS) : null,
'useSSL' => App::env('REDIS_USE_SSL'),
'contextOptions' => [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
],
];
// Instantiate and return it
return Craft::createObject($config);
},
],
];
In my local environment, I access Redis in a Docker container on my local machine. It doesn’t require any credentials since it’s local. I also set the useSSL
value to false
.
REDIS_URL=redis://:@redis:6379
REDIS_USE_SSL=false
The Heroku equivalent is something like the example you see below. It will change over time, so be sure to use the env variable they provide so your configuration remains current as they update the account credentials. I also set the useSSL
value to true
.
During debugging, I used this remote Redis variable locally to do my debugging but changed it back to the Redis in my Docker container after I got it configured correctly for my Heroku app.
REDIS_URL=rediss://:somelongstringfromamazon123456@ec2-12-345-678-901.compute-1.amazonaws.com:12345
REDIS_USE_SSL=true
Some useful commands and an example Github repo
I’ll wrap this up with some helpful commands. There’s a good chance you know these, but they were helpful in the discovery process I outlined above.
To see the real-time logs from your Heroku app, use this command.
heroku logs --tail -a my-app-name
See the Redis logs only
heroku logs -s redis -t -a my-app-name
Finally, check out my Craft CMS Heroku Starter repo on Github.
Good luck with configuring your Heroku app. I hope this helped out.