Deploying a Spring Boot backend should be simple in theory. Build the JAR, set the environment variables, connect the database, and ship it.
In practice, my deployment exposed several assumptions that worked locally but failed immediately in the cloud.
I recently deployed a modular Spring Boot application to Render using Docker, Render Blueprint, PostgreSQL, Redis, Flyway migrations, Spring profiles, Hibernate/JPA, and environment variables.
The application worked locally with MySQL and Redis, but deployment exposed several production-specific issues that were easy to miss in local development. This article documents the problems, why they happened, and how I fixed them properly.
Who This Article Is For
This article is useful if you are deploying a Spring Boot application to Render and your local setup uses MySQL, Redis, Flyway, Docker, or a multi-module Maven structure.
It is especially relevant if you are moving from a local MySQL setup to PostgreSQL in the cloud.
The Stack
The backend was a Java 17 Spring Boot application with multiple Maven modules:
alagbafo/
├── api-contracts
├── core
├── users
├── orders
├── payments
├── wallet
├── notifications
├── admin
├── subscriptions
└── app
The app module was the actual Spring Boot entry point.
Locally, the project used MySQL and Redis:
spring.datasource.url=jdbc:mysql://localhost:3306/alagbafo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.data.redis.host=localhost
spring.data.redis.port=6379
For Render, the target setup was:
Spring Boot app
PostgreSQL database
Redis-compatible Key Value store
Docker deployment
Flyway migrations
Render Blueprint was the best fit because it allowed the infrastructure to be described in a render.yaml file.
Step 1: Dockerfile for a Multi-Module Spring Boot App
Because the project was a multi-module Maven application, the Dockerfile had to copy all module pom.xml files before copying the source code.
This improves Docker layer caching because dependencies can be downloaded before the full source code is copied.
# Stage 1: Build
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
COPY api-contracts/pom.xml api-contracts/
COPY core/pom.xml core/
COPY users/pom.xml users/
COPY orders/pom.xml orders/
COPY payments/pom.xml payments/
COPY wallet/pom.xml wallet/
COPY delivery/pom.xml delivery/
COPY notifications/pom.xml notifications/
COPY admin/pom.xml admin/
COPY support/pom.xml support/
COPY packages/pom.xml packages/
COPY subscriptions/pom.xml subscriptions/
COPY app/pom.xml app/
RUN mvn dependency:go-offline -B
COPY . .
RUN mvn clean package -DskipTests -B
# Stage 2: Run
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=build /app/app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
This works well for Render because Render can build directly from the Dockerfile.
Step 2: The Render Blueprint
The deployment needed three resources:
- A web service
- A PostgreSQL database
- A Redis service
The corrected render.yaml looked like this:
services:
- type: web
name: alagbafo
runtime: docker
dockerfilePath: ./Dockerfile
plan: free
envVars:
- key: SPRING_PROFILES_ACTIVE
value: render
- key: JAVA_OPTS
value: "-Xms256m -Xmx512m"
- key: DATABASE_URL
fromDatabase:
name: alagbafo-db
property: connectionString
- key: REDIS_URL
fromService:
type: redis
name: alagbafo-redis
property: connectionString
- key: JWT_SECRET
generateValue: true
- key: APP_BASE_URL
value: "https://alagbafo.onrender.com"
- key: PAYSTACK_SECRET_KEY
sync: false
- key: PAYSTACK_PUBLIC_KEY
sync: false
- key: PAYSTACK_WEBHOOK_SECRET
sync: false
- type: redis
name: alagbafo-redis
plan: free
ipAllowList: []
databases:
- name: alagbafo-db
plan: free
databaseName: alagbafo
ipAllowList: []
One important lesson: Redis does not belong under databases.
PostgreSQL goes under databases, while Redis goes under services with type: redis.
If Redis is declared incorrectly, Render will not inject REDIS_URL, and Spring Boot will fail at startup.
Step 3: Creating a Render-Specific Spring Profile
The first deployment failed with this:
Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
That meant the application was still using the default MySQL configuration.
The cause was simple: the Render Spring profile was not active.
The fix was to create a Render-specific profile:
# application-render.properties
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.flyway.locations=classpath:db/migration-pg
server.port=${PORT:8080}
Then I made sure Render set this environment variable:
SPRING_PROFILES_ACTIVE=render
Without this, Spring Boot keeps loading application.properties, which in my case pointed to MySQL.
Problem 1: Redis URL Was Empty
The next failure was:
The URL '' is not valid for configuring Spring Data Redis.
The scheme 'null' is not supported.
Use the scheme 'redis://' for insecure or 'rediss://' for secure Redis standalone configuration.
This happened because the Redis service was declared incorrectly in render.yaml.
I initially put Redis under databases, which was wrong.
The corrected Redis service definition was:
services:
- type: redis
name: alagbafo-redis
plan: free
ipAllowList: []
Then the web service can reference it:
- key: REDIS_URL
fromService:
type: redis
name: alagbafo-redis
property: connectionString
In Spring Boot, the Render profile uses:
spring.data.redis.url=${REDIS_URL}
Avoid using an empty fallback like this:
spring.data.redis.url=${REDIS_URL:}
That makes debugging harder because Spring receives an empty string and fails with a confusing URL error.
Problem 2: Render PostgreSQL URL Is Not a JDBC URL
Render provides PostgreSQL URLs like this:
postgresql://user:password@host/database
The PostgreSQL JDBC driver expects something like this:
jdbc:postgresql://host:5432/database
At first, I tried this:
spring.datasource.url=jdbc:${DATABASE_URL}
That produced a URL like this:
jdbc:postgresql://user:password@host/database
It looked close, but it still failed:
Driver org.postgresql.Driver claims to not accept jdbcUrl
The issue was that the JDBC URL should not include credentials in that URI format.
Spring/Hikari works better with this structure:
jdbc:postgresql://host:5432/database
username=user
password=password
So I created a Render-only datasource config:
package com.alagbafo.core.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import javax.sql.DataSource;
import java.net.URI;
@Configuration
@Profile("render")
@ConditionalOnProperty(name = "DATABASE_URL")
public class RenderDataSourceConfig {
@Bean
public DataSource dataSource(@Value("${DATABASE_URL}") String databaseUrl) {
URI uri = URI.create(databaseUrl);
String[] userInfo = uri.getUserInfo().split(":", 2);
String username = userInfo[0];
String password = userInfo.length > 1 ? userInfo[1] : "";
int port = uri.getPort() == -1 ? 5432 : uri.getPort();
String jdbcUrl = "jdbc:postgresql://" + uri.getHost() + ":" + port + uri.getPath();
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(jdbcUrl);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName("org.postgresql.Driver");
return dataSource;
}
}
This solved the datasource issue cleanly.
Step 4: Adding PostgreSQL Dependencies
The project originally only had MySQL:
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
For Render, I added PostgreSQL:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
Since Flyway was being used, I also added PostgreSQL database support:
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
Problem 3: MySQL Flyway Migrations Do Not Run on PostgreSQL
The local migrations used MySQL syntax:
CREATE TABLE users_user (
id BIGINT NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
created_at DATETIME(6) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_users_user_email (email),
KEY idx_user_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
PostgreSQL does not understand MySQL-specific syntax like:
AUTO_INCREMENT
DATETIME(6)
UNIQUE KEY
KEY
ENGINE=InnoDB
DEFAULT CHARSET
COLLATE=utf8mb4_unicode_ci
ON DUPLICATE KEY UPDATE
So I created a separate migration folder for PostgreSQL:
core/src/main/resources/db/migration-pg
Then the Render profile pointed Flyway there:
spring.flyway.locations=classpath:db/migration-pg
A PostgreSQL-compatible version looks like this:
CREATE TABLE users_user (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP(6) NOT NULL,
CONSTRAINT uk_users_user_user_id UNIQUE (user_id),
CONSTRAINT uk_users_user_email UNIQUE (email)
);
CREATE INDEX idx_user_created ON users_user (created_at);
For MySQL upserts like this:
ON DUPLICATE KEY UPDATE
PostgreSQL uses:
ON CONFLICT (code) DO UPDATE SET
name = EXCLUDED.name,
description = EXCLUDED.description;
Problem 4: JSON vs JSONB vs Hibernate Validation
After database connectivity was solved, Hibernate schema validation failed:
Schema-validation: wrong column type encountered in column [new_value]
in table [admin_audit_log];
found [jsonb (Types#OTHER)], but expecting [json (Types#VARCHAR)]
The entity had:
@Column(name = "new_value", columnDefinition = "JSON")
private String newValue;
But the PostgreSQL migration had created:
new_value JSONB
PostgreSQL supports both json and jsonb, but Hibernate was validating strictly against the entity definition.
I fixed the PostgreSQL migration:
old_value JSON,
new_value JSON
Because the old migration had already run on Render, I added a corrective migration:
ALTER TABLE admin_audit_log
ALTER COLUMN old_value TYPE JSON USING old_value::JSON,
ALTER COLUMN new_value TYPE JSON USING new_value::JSON;
ALTER TABLE notifications
ALTER COLUMN template_variables TYPE JSON USING template_variables::JSON;
Problem 5: Hibernate Validation Can Be Too Strict for Deployment
In local development, this was useful:
spring.jpa.hibernate.ddl-auto=validate
In a production-style environment with Flyway, I changed it to:
spring.jpa.hibernate.ddl-auto=none
This is the better setup when Flyway owns schema management.
The final Render profile became:
# application-render.properties
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false
spring.flyway.locations=classpath:db/migration-pg
spring.flyway.validate-on-migrate=false
spring.data.redis.url=${REDIS_URL}
server.port=${PORT:8080}
Flyway handles migrations. Hibernate runs the app. They do not fight each other.
Environment Variables on Render
These were required:
SPRING_PROFILES_ACTIVE=render
DATABASE_URL=auto-injected by Render PostgreSQL
REDIS_URL=auto-injected by Render Redis
JWT_SECRET=secure-generated-secret
Paystack could be added later, but the app expected placeholders during setup:
PAYSTACK_SECRET_KEY=sk_test_dummy
PAYSTACK_PUBLIC_KEY=pk_test_dummy
PAYSTACK_WEBHOOK_SECRET=dummy
For a secure JWT secret, generate one locally:
openssl rand -base64 48
Final Checklist
If you are deploying a Spring Boot app to Render with Blueprint, PostgreSQL, Redis, Docker, and Flyway, check these:
- Use
New Blueprint, not justNew Web Service. - Put PostgreSQL under
databases. - Put Redis under
serviceswithtype: redis. - Set
SPRING_PROFILES_ACTIVE=render. - Do not use MySQL migrations for PostgreSQL.
- Use separate Flyway folders for database-specific migrations.
- Convert Render
postgresql://...URL to JDBC format. - Let Flyway manage schema.
- Set
spring.jpa.hibernate.ddl-auto=nonein deployment. - Add dummy values for optional third-party secrets if needed.
- Keep real secrets in Render environment variables, not code.
Final Thoughts
The frustrating part was that every fix revealed the next layer.
First, MySQL was still being used. Then Redis was not injected. Then the PostgreSQL URL format was wrong. Then Flyway migrations needed PostgreSQL syntax. Then Hibernate schema validation complained about JSON types.
But each failure made the deployment more production-ready.
The key lesson is this: local development hides infrastructure assumptions, while cloud deployment exposes them.
Once the app had a proper Render profile, correct Blueprint config, PostgreSQL-specific Flyway migrations, and a datasource parser for Render’s database URL, the deployment became predictable.
That was the real win.
Top comments (0)