惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
雷峰网
雷峰网
有赞技术团队
有赞技术团队
罗磊的独立博客
博客园 - 叶小钗
Jina AI
Jina AI
博客园 - 司徒正美
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
Apple Machine Learning Research
Apple Machine Learning Research
阮一峰的网络日志
阮一峰的网络日志
Microsoft Security Blog
Microsoft Security Blog
大猫的无限游戏
大猫的无限游戏
量子位
MyScale Blog
MyScale Blog
V
Visual Studio Blog
博客园 - 聂微东
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
小众软件
小众软件
宝玉的分享
宝玉的分享

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Deploying a Multi-Module Spring Boot App to Render with P...
Ojo Ilesanmi · 2026-06-23 · via DEV Community

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 just New Web Service.
  • Put PostgreSQL under databases.
  • Put Redis under services with type: 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=none in 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.