- Published on
Getting Spring Boot, Docker and Redis working on Heroku
- Authors
- Name
- Yair Mark
- @yairmark
I was working on a project recently and needed to get the app running on a demo server as fast and easily as possible. Heroku delivered on this although I had to tweak a few things to get it working 100%.
Bare Bones Heroku Docker Setup
The bare minimum to get Heroku picking up your Dockerfile is:
- heroku.yml
- Dockerfile
- Any environment variables you need
This should work for any type of project, not just Spring/Java.
My heroku.yml
looks as follows:
setup:
addons:
- plan: Heroku-redis
as: CACHE
build:
docker:
web: Dockerfile
Note:
- If you have a
heroku.yml
file theProcfile
is ignored. Hence no need for aProcfile
- You can leave out the
setup
section if you have no addons.- In my case I have a Redis addon for cache management.
My Dockerfile is nothing special it looks as follows:
FROM gradle:6.3.0-jdk8 as builder
WORKDIR /home/builder
COPY . .
RUN gradle build -x test
FROM OpenJDK:8-alpine as runner
WORKDIR /home
COPY /home/builder/build/libs/whatsApp-channel-0.0.1-SNAPSHOT.jar /home
ENTRYPOINT ["java","-jar","my-awesome-app-0.0.1-SNAPSHOT.jar"]
Note:
- This uses multi-stage builds which work on Heroku. This eliminates the need for a docker registry to store your images.
- The Dockerfile has to be in the root of your repo. This is not configurable on Heroku's side
For the environment variables:
- Go to your app's "Settings" tab
- Click "Reveal Config Vars"
- Add environment variables with the name that your app is expecting. Associate any values you need to.
What was not clear to me was whether or not Heroku passes these to my app. The short answer is yes it does.
Heroku also exposes some special environment variables:
- One per Heroku addon
- In my case there was a URI to my Redis addon
PORT
- This will not show up in your config section
- This is the port Heroku expects your app to be exposed on.
- You will need to update your app to use this port.
- If you do not use this port Heroku gives you routing errors when trying to hit your app.
Spring Specific Changes
Initially I ran into the following error when the app started up:
Caused by: org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR Unsupported CONFIG parameter: notify-keyspace-events
To get this to work I had to update my Redis config to not do any startup configuration by returning the NO_OP operation. My full Redis config is below:
package com.example.app.config
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig
import org.springframework.data.redis.cache.RedisCacheManager
import org.springframework.data.redis.connection.RedisPassword
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.session.data.redis.config.ConfigureRedisAction
import java.net.URI
@Configuration
class SpringSessionRedisConfiguration {
@Value("\${REDIS_URL}")
private lateinit var redisUriString: String
@Bean
fun configureRedisAction(): ConfigureRedisAction? {
return ConfigureRedisAction.NO_OP
}
data class RedisLoginDetails(val host: String, val port: Int, val password: String?)
fun getRedisLoginDetails(redisToGoString: String): RedisLoginDetails {
val uri = URI(redisToGoString)
return RedisLoginDetails(
host = uri.host,
port = uri.port,
password = uri.userInfo?.split(":")?.getOrElse(1) { "" } ?: ""
)
}
@Bean
fun jedisConnectionFactory(): JedisConnectionFactory {
val (host, port, password) = getRedisLoginDetails(redisUriString)
val standaloneConfiguration = RedisStandaloneConfiguration(host, port)
standaloneConfiguration.password = RedisPassword.of(password)
return JedisConnectionFactory(standaloneConfiguration)
}
@Bean
fun redisTemplate(): RedisTemplate<Any, Any> {
val redisTemplate = RedisTemplate<Any, Any>()
redisTemplate.setConnectionFactory(jedisConnectionFactory())
return redisTemplate
}
@Bean
fun cacheManager(jedisConnectionFactory: JedisConnectionFactory): RedisCacheManager {
return RedisCacheManager
.builder(jedisConnectionFactory)
.cacheDefaults(defaultCacheConfig())
.transactionAware()
.build()
}
}
I configured Spring to use the environment variable PORT
if it is present otherwise server.port
and finally fallback to 8080
if none are present:
package com.example.app.config
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.web.server.ConfigurableWebServerFactory
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.context.annotation.Configuration
import org.springframework.core.env.Environment
@Configuration
class ServerPortCustomizer : WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Autowired
lateinit var environment: Environment
override fun customize(factory: ConfigurableWebServerFactory) {
val port: String? = environment.getProperty("PORT")
val serverPort: String? = environment.getProperty("server.port")
val portToUse = port?.toInt() ?: (serverPort?.toInt() ?: 8080)
factory.setPort(portToUse)
}
}
Resources
Article on Setting up Spring Boot, Redis and Heroku - this has a section where the author parses the Redis to go URI. In your local config if you use localhost you will need to change it to a URI as follows:
Redis://localhost:60379
Common Causes of Heroku's Routing Error
- In my case I had to pass the port from Heroku down to the app, this is the environment variable
PORT
- This article explains different approaches to do this
- I used the programmatic approach where I add a new component to configure the port if it is present or fallback to 8080 if not.
- This article explains different approaches to do this
- In my case I had to pass the port from Heroku down to the app, this is the environment variable