Career OS

Getting SplitEase Online

You built splitease-api — Spring Boot, PostgreSQL, JWT. It runs on localhost:8080 and nobody but you can reach it. A recruiter asking “do you have anything deployed?” wants a URL they can curl, not a screenshot. This module gets your API live on the public internet for zero rupees, and — just as important — teaches you exactly what the paid AWS version of every step would be, so the knowledge transfers the day a job hands you an AWS account.

The Goal

By the end of this module you can:

  • Deploy splitease-api to a free PaaS (Railway or Render) straight from GitHub, with a managed Postgres attached
  • Map every PaaS click to its AWS equivalent — so “deploy a container + a database” is the same idea whether the bill is zero or four figures
  • Inject secrets and config as environment variables in the cloud instead of committing them — the JWT_SECRET never leaves the dashboard
  • Connect your app to a managed PostgreSQL the cloud runs, backs up, and patches for you
  • Read the platform’s health checks and logs to prove the deploy is alive and to debug it when it is not

The Lesson

Two honest paths up the mountain

There are two ways to get a Spring Boot API onto the internet, and a beginner should not start with the hard one.

PaaS (Railway / Render)Raw cloud (AWS)
What you give itYour GitHub repoA container image, plus servers, network, database, all wired by you
Who runs the serverThe platformYou, on EC2 or Elastic Beanstalk
Who runs PostgresThe platform (one click)You, on RDS (many clicks)
Time to first live URL~15 minutesa focused afternoon, first time
Cost for a learnerFree tier covers itFree tier exists but easy to overshoot
What you learnDeploy mechanics, env vars, logsAll of that plus IAM, VPC, security groups

The skill — code lives in git, the platform builds it, secrets come from the environment, a managed database holds the data, health checks and logs tell you it is alive — is identical on both. PaaS just pre-decides the boring parts. You learn the concepts on the free path, then carry them onto AWS when a job requires it. That mapping is the whole point of this module, so every PaaS step below is paired with its AWS twin.

The deployed topology — what you are actually building

Whichever path, the shape is the same: a build step produces a runnable artifact, the platform runs it, it talks to a managed database, and the internet reaches it through a front door that terminates HTTPS.

flowchart LR
    Dev[You push to GitHub main] --> Build[Platform builds the image from your Dockerfile]
    Build --> App[Running splitease-api container]
    Internet[Public user with curl or browser] -->|HTTPS| Edge[Platform edge - TLS and routing]
    Edge --> App
    App -->|JDBC over private network| DB[(Managed PostgreSQL)]
    Env[Environment variables - JWT secret and DB url] -.injected at start.-> App
    App -->|writes logs| Logs[Platform log stream]
    Edge -->|periodic GET on health path| App

Read it as a sentence: a push triggers a build, the build becomes a running container, the public reaches it over HTTPS through the platform’s edge, the container reaches a managed Postgres over a private link, secrets arrive as environment variables at startup, the container streams logs, and the edge keeps poking a health path to decide whether the container is fit to receive traffic. Every box has an AWS name, and we will name them.

Step 1 — get a build artifact the cloud can run

You already did the hard part in the Docker track: a multi-stage Dockerfile that produces a small JRE image with your JAR inside. A container image is the universal currency of deployment — it runs the same on Railway, Render, or AWS, which is the entire reason Docker exists.

Railway and Render can both build straight from that Dockerfile. If your repo has no Dockerfile, Render can also detect a Spring Boot project and build it with a “native” buildpack — but the Dockerfile path is the one worth knowing, because it is the path AWS uses too.

Real check before you deploy: docker build -t splitease-api . && docker run -p 8080:8080 splitease-api on your laptop. If it does not run in a container locally, it will not run in a container in the cloud. Fix it here, where the feedback loop is seconds.

AWS twin: that same image gets pushed to ECR (Elastic Container Registry — AWS’s private Docker Hub), and App Runner or Elastic Beanstalk or ECS pulls and runs it. Same image, different runner.

Step 2 — managed Postgres, not a database you babysit

Your app needs PostgreSQL. You will not install Postgres on a server and maintain it. You will rent a managed one: the platform runs it, patches it, backs it up, and hands you a connection string.

On Railway: “New → Database → PostgreSQL.” On Render: “New → PostgreSQL.” Within seconds you have a live database and a connection URL that looks like:

postgresql://splitease_user:somelongpassword@some-host.internal:5432/splitease

That URL is a secret — it contains the password. It belongs in environment variables, never in application.properties committed to git.

Why managed matters: the day your single database disk fills or the host dies, “managed” means the platform already has automated backups and can restore. Self-managed means you are the backup, at 2am. This is the single highest-value thing the cloud sells you — see system design’s data layer for why the database is the part you least want to hand-roll.

AWS twin: this is RDS (Relational Database Service) for PostgreSQL. Exactly the same idea — AWS runs the Postgres, you get an endpoint — with more knobs: instance size, storage, multi-AZ failover, automated snapshots. A PaaS database is RDS with the knobs pre-set for you. (You met RDS already in the troubleshooting note about the SUPERUSER privilege on import — managed Postgres deliberately does not give you superuser, and that is a feature, not a bug.)

Step 3 — config and secrets as environment variables

Here is the rule that survives every platform: configuration that changes between environments, and every secret, comes from the environment — not the code. This is principle III of the Twelve-Factor App, and it is why Spring Boot lets any property be overridden by an environment variable.

Spring maps env vars to properties by upper-casing and replacing dots with underscores:

application.properties keyEnvironment variable
spring.datasource.urlSPRING_DATASOURCE_URL
spring.datasource.usernameSPRING_DATASOURCE_USERNAME
spring.datasource.passwordSPRING_DATASOURCE_PASSWORD
app.jwt.secret (your custom key)APP_JWT_SECRET

So in the Railway/Render dashboard you set:

SPRING_DATASOURCE_URL      = jdbc:postgresql://some-host.internal:5432/splitease
SPRING_DATASOURCE_USERNAME = splitease_user
SPRING_DATASOURCE_PASSWORD = somelongpassword
APP_JWT_SECRET             = <a fresh 256-bit random key, NOT the one from your laptop>

Note the jdbc: prefix — Postgres hands you a postgresql:// URL, but the JDBC driver wants jdbc:postgresql://. A classic five-minute confusion. Railway lets you reference the database’s own variables, so SPRING_DATASOURCE_URL can be built from the database service rather than pasted.

The JWT secret deserves its own line: generate a brand-new one for production. The secret in your dev application.properties has been sitting in a git history and on your disk — treat it as compromised. Anyone with your signing secret can forge a valid token and log in as any user. The full why is in Spring Boot’s security module; the operational rule is simply: prod gets its own secret, set only in the dashboard, never echoed into logs.

AWS twin: environment variables on the service work the same, but for real secrets AWS gives you Secrets Manager or SSM Parameter Store (encrypted, access-controlled, auditable, rotatable) and the app reads them at boot. A PaaS env-var box is the beginner version of Secrets Manager. The principle — secrets out of code, into the environment — does not change.

Step 4 — health checks, the platform’s heartbeat

The platform needs a way to ask your app “are you actually ready?” That is a health check: a URL the platform hits on a schedule. If it returns 200, the instance is healthy and gets traffic; if it fails repeatedly, the platform restarts the instance or stops sending it requests.

Spring Boot Actuator gives you this for free. Add the dependency, then:

management.endpoints.web.exposure.include=health
management.endpoint.health.probes.enabled=true

Now GET /actuator/health returns:

{"status":"UP"}

Point the platform’s health check at /actuator/health. Crucially, Actuator’s health includes the database — if Postgres is unreachable, the status flips to DOWN and you find out from the health check instead of from a user hitting a 500. Set the path in Railway under the service settings, or in Render’s “Health Check Path” field.

sequenceDiagram
    participant Edge as Platform edge
    participant App as splitease-api
    participant DB as Managed Postgres
    Edge->>App: GET /actuator/health
    App->>DB: SELECT 1 (connection check)
    DB-->>App: ok
    App-->>Edge: 200 status UP
    Note over Edge,App: healthy - keep sending traffic
    Edge->>App: GET /actuator/health
    App->>DB: SELECT 1
    DB--xApp: connection refused
    App-->>Edge: 503 status DOWN
    Note over Edge,App: unhealthy - restart or stop routing

AWS twin: on Elastic Beanstalk and App Runner you configure a health-check path exactly the same way; on a load balancer (ALB) it is the “target group health check.” Same heartbeat, same /actuator/health, same outcome — unhealthy targets stop receiving traffic.

Step 5 — logs are your eyes in production

Your app no longer has a terminal you can watch. Everything it prints to stdout/stderr becomes the platform’s log stream. Railway: the “Deployments → Logs” tab. Render: the “Logs” tab. This is where you watch the boot sequence, see the “Started SpliteaseApiApplication in 4.2 seconds” line that means success, and read the stack trace when it does not.

The single most common first-deploy failure is the app crashing on boot because it cannot reach the database — wrong SPRING_DATASOURCE_URL, missing jdbc: prefix, or the password copied with a trailing space. The logs say it plainly:

org.postgresql.util.PSQLException: Connection to some-host:5432 refused

Read it like you read a stack trace in Core Java 01: the exception type tells you the category (connection), the message tells you the where (host:port). You fix it in the env vars, redeploy, watch the logs again.

AWS twin: stdout goes to CloudWatch Logs. Same model — your app prints, the platform captures, you read. CloudWatch adds searchable log groups and alarms on log patterns, which is the grown-up version of scrolling the Render logs tab.

The full mapping, on one card

This table is the takeaway. Learn the left column for free on PaaS; the right column is the same idea with a job’s AWS account behind it.

ConceptRailway / Render (free)AWS equivalent
Run my containerThe service you deployElastic Beanstalk / App Runner / ECS on EC2 or Fargate
Store the imageBuilt internallyECR
Managed databaseOne-click PostgreSQLRDS for PostgreSQL
Secrets / configService env varsSecrets Manager / SSM Parameter Store (+ env vars)
HTTPS front doorAutomatic platform domainApplication Load Balancer + ACM certificate
Health checkHealth check path settingALB target-group / Beanstalk health check
LogsLogs tabCloudWatch Logs
Deploy on pushGitHub integrationCodePipeline, or GitHub Actions to any of the above

The classic trap: a junior is told “just put it on AWS” and freezes because the console has 200 services. The cure is exactly this table — you are not learning 200 services, you are learning seven concepts that happen to have AWS-branded names.

Check The Concept

How This Shows Up At Work

  • The “do you have anything live?” interview moment. A recruiter or interviewer asks for a deployed URL. The candidate who hands over https://splitease-api.up.railway.app/actuator/health returning {"status":"UP"} is instantly more credible than the one with code that “runs locally.” This is the cheapest credibility you will ever buy.
  • The boot-loop incident. A deploy goes red and restarts forever. A teammate panics; you open the logs tab, see Connection to db:5432 refused, check the SPRING_DATASOURCE_URL env var, spot a missing jdbc: prefix, fix it, redeploy. Reading logs is 80% of operating a deployed app.
  • The secret in the diff. In code review someone has pasted a real database password into application.properties. You block the PR, explain Twelve-Factor config, and point them at the env-var path. On a public repo this is a security incident, not a style nit — the repo for this very site is public, which is why none of its examples carry real secrets.
  • The “move it to AWS” task. Six months in, a job asks you to migrate the same app to AWS. Because you learned the concepts, not just the Railway buttons, you map service→Beanstalk, Postgres→RDS, env vars→Secrets Manager, and the task is translation, not learning from zero.

Build This

You are taking splitease-api from localhost to a public URL. Pick one platform (Railway or Render — both free); the steps are near-identical. Everything is browser + PowerShell.

  1. Confirm it runs in a container locally first. In the repo root, on Windows PowerShell:
docker build -t splitease-api .
docker run -p 8080:8080 `
  -e SPRING_DATASOURCE_URL="jdbc:postgresql://host.docker.internal:5432/splitease_sql" `
  -e SPRING_DATASOURCE_USERNAME="postgres" `
  -e SPRING_DATASOURCE_PASSWORD="postgres" `
  -e APP_JWT_SECRET="local-dev-secret-change-me" `
  splitease-api

You want to see Started SpliteaseApiApplication. If it crashes here, fix it before touching the cloud — the loop is seconds, not minutes.

  1. Push the repo to GitHub if it is not already there. The platform deploys from GitHub.

  2. Create the database. Railway: New → Database → PostgreSQL. Render: New → PostgreSQL (free instance). Copy the connection details it shows you.

  3. Create the service. Point the platform at your GitHub repo. It detects the Dockerfile and builds. Watch the build logs scroll.

  4. Set the environment variables in the service settings — the four from the Lesson. Build SPRING_DATASOURCE_URL from the database’s host/port/name with the jdbc: prefix, and generate a fresh APP_JWT_SECRET:

# a quick 256-bit random secret, base64
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 }))
  1. Set the health check path to /actuator/health in the service settings, then redeploy.

  2. Watch the logs. Open the Logs tab. You are looking for Started SpliteaseApiApplication in N seconds and the absence of any PSQLException.

  3. Prove it is live from your own machine — replace the host with your real URL:

curl https://your-app.up.railway.app/actuator/health

Expected:

{"status":"UP"}

Then register and log in against the live URL using the curl calls from week 9 day 5. You now have an API on the public internet.

  1. Break it on purpose #1 — kill the DB connection. In the env vars, change one character of SPRING_DATASOURCE_PASSWORD. Redeploy. Watch the logs: the app fails to boot with a connection/authentication error, and /actuator/health either never comes up or returns DOWN. This is the most common real deploy failure, manufactured on purpose so you recognise it instantly next time. Fix the password, redeploy, confirm UP.

  2. Break it on purpose #2 — wrong health path. Set the health check path to /healthz (which your app does not serve). Redeploy. The platform marks the deploy unhealthy even though the app is fine — because the heartbeat is checking a door that does not exist. Read what “unhealthy” looks like in the dashboard, then set it back to /actuator/health.

Interview Practice

These are the deployment questions that actually come up for backend/fintech roles at Indian product companies — usually right after “tell me about a project you deployed.”

Q1 — Walk me through how you would deploy a Spring Boot app to production.

Package it as a container image (multi-stage Dockerfile so the final image is just JRE + JAR). Push the image to a registry, or let a platform build it from the repo. Provision a managed database and get its connection string. Inject the database URL, credentials, and any secrets (like the JWT signing key) as environment variables — never in the committed config. Configure a health-check endpoint (/actuator/health via Actuator) so the platform knows when the instance is ready and can restart it if it dies. Deploy, then watch the logs to confirm a clean boot and to debug if not. On AWS the same shape is ECR + Elastic Beanstalk/App Runner + RDS + Secrets Manager + an ALB health check + CloudWatch logs.

Q2 — Where do secrets like the database password and JWT key go, and why not in the code?

In environment variables (or a secrets manager), injected at runtime — never in source or committed config. Code is shared, versioned, and often public; a secret in git is exposed forever in history even if you delete it later. This is the config principle of the Twelve-Factor App: config that varies by environment lives in the environment. For real systems use a dedicated secrets store (AWS Secrets Manager / SSM Parameter Store) that is encrypted, access-controlled, audited, and rotatable. And production gets its own freshly generated secrets — never reuse the dev JWT key, since anyone holding the signing key can forge valid tokens.

Q3 — What is a managed database and why use one instead of running Postgres yourself?

A managed database (RDS on AWS, or a PaaS one-click Postgres) is one the provider runs for you: provisioning, patching, automated backups, and often failover and scaling. You get an endpoint and credentials; you do not maintain the server. The reason is risk and time: the database is the one component you least want to hand-roll, because a lost disk or unpatched vulnerability there means lost data. Managed means the backup-and-restore machinery already exists. Self-managed means you are the on-call DBA.

Q4 — What is a health check and what happens when it fails?

A health check is an endpoint the platform polls on a schedule to decide if an instance is fit to serve traffic. Returning 200 means healthy; repeated failures mean the platform stops routing requests to that instance and/or restarts it. In Spring Boot, Actuator’s /actuator/health provides this and includes downstream checks like database connectivity, so if Postgres is unreachable the health flips to DOWN — letting the platform react before users hit errors. On AWS it is the load balancer’s target-group health check or the Beanstalk/App Runner health path.

Q5 — Your deploy keeps crashing on startup. How do you debug it?

Open the logs — the deployed app has no terminal, so its stdout/stderr is the log stream. Read the stack trace like any other: exception type for the category, message for the where. The most common boot failure is the app cannot reach the database — a wrong SPRING_DATASOURCE_URL, a missing jdbc: prefix, or a mistyped password produces a PSQLException: connection refused/authentication failed. Fix the env var, redeploy, watch the logs again. If it is not the DB, the trace names the actual bean or config that failed.

Q6 — When would you move off a PaaS like Railway onto raw AWS?

When you hit something the PaaS cannot give you cheaply or at all: needing a private network (VPC) for compliance, fine-grained IAM, specific regions for data residency (relevant for Indian fintech under RBI data-localisation rules), cost at scale where the PaaS markup exceeds the ops effort of running it yourself, or services the PaaS does not offer. Until then a PaaS is the right call — it pre-decides the undifferentiated heavy lifting so you ship faster. The decision is covered more fully in the next module.

Q7 — Why package the app as a Docker image instead of just deploying the JAR?

The image bundles the exact JRE version, OS libraries, and your JAR into one immutable artifact that runs identically on your laptop, CI, and every cloud — it kills “works on my machine.” It is also the universal deploy currency: the same image runs on Railway, Render, ECS, or Kubernetes with no change. A bare JAR depends on whatever Java the host happens to have; the image carries its own.

Where to Practice

ResourceWhat to doHow long
railway.app docs / render.com docsFollow the “deploy from GitHub” and “add a PostgreSQL database” guides while deploying your own splitease-api45 min
docs.docker.comRe-read “Multi-stage builds” with your own Dockerfile open, then run the local container test from Build This20 min
aws.amazon.com/freeRead the RDS and Elastic Beanstalk free-tier pages — just to map the names you learned to AWS services, no account needed20 min
learn.microsoft.comSkim “Azure App Service overview” to see the same PaaS concepts under a third provider’s branding15 min

Check Yourself

  1. Name the seven concepts that are identical whether you deploy on PaaS or AWS.
  2. What does SPRING_DATASOURCE_URL need that the raw postgresql:// URL from the database does not have?
  3. Why must production get a freshly generated JWT secret instead of the one in your dev config?
  4. What does /actuator/health check besides “is the process running,” and why does that matter?
  5. Your deploy boot-loops. What is the first thing you open, and what is the most likely cause?
  6. Map these to AWS: the running container, the managed Postgres, the secrets, the log stream.
  7. What is the role of the Docker image in making the same app run on Railway and on AWS unchanged?
  8. Give one concrete reason a team would move off a PaaS onto raw AWS.
Answers
  1. Run my container, store the image, managed database, secrets/config from the environment, an HTTPS front door, a health check, and logs.
  2. The jdbc: prefix — the JDBC driver needs jdbc:postgresql://.... The username and password also move into their own SPRING_DATASOURCE_USERNAME / PASSWORD variables.
  3. The dev secret has lived on disk and in git history, so treat it as compromised. Anyone holding the signing key can forge a valid token and authenticate as any user.
  4. It checks downstream dependencies, notably database connectivity — so an unreachable Postgres flips health to DOWN. That lets the platform stop routing or restart the instance before users start hitting 500s.
  5. The logs (Railway/Render Logs tab). The most likely cause is the app cannot reach the database: a wrong URL, a missing jdbc: prefix, or a bad password producing a PSQLException.
  6. Container → Elastic Beanstalk / App Runner / ECS; managed Postgres → RDS; secrets → Secrets Manager / SSM Parameter Store; log stream → CloudWatch Logs.
  7. The image bundles the exact JRE, OS libraries, and your JAR into one immutable artifact, so it runs identically everywhere — it is the universal deploy currency that removes “works on my machine.”
  8. Any of: needing a VPC/private network, fine-grained IAM, specific regions for data residency (RBI localisation), cost at large scale, or a service the PaaS does not offer.

Explain it out loud: Explain to an empty chair the full journey of one request to your live API — from curl on your laptop, through the platform’s HTTPS edge, into the running container, out to managed Postgres, and back — then say what the AWS name is for each box along the way. If you stall on any box’s AWS twin, re-read the mapping card.

Why AI Can’t Do This For You

AI can generate a Dockerfile and even a Railway config in seconds. What it cannot do is stand in front of your specific red deploy at 11pm, read your logs, and know that the PSQLException is because you fat-fingered the password in the env var — not the code AI wrote, which is fine. Deployment failures are almost never about the code; they are about the gap between the code and the specific environment it landed in, and that gap is invisible to a model that cannot see your dashboard.

The transferable judgment — that a PaaS button and an AWS service are the same concept, that a secret in a diff is an incident, that a health check failing means check the door before you blame the app — is built by deploying your own thing, breaking it on purpose, and reading the logs until the failure patterns are reflexes. No prompt installs that reflex.

Module done? Add it to today’s tracker

Saves your progress on this device.