De-Ploi-er —> A Mini Platform-as-a-Service
A lightweight PaaS (Platform-as-a-Service) built with Spring Boot that lets you deploy any Dockerized application from a GitHub repository with a single API call. It clones, builds, and runs your app behind a Traefik reverse proxy — each project gets its own subdomain at <project-name>.localhost.
┌─────────────────────────────────────────────────────────────────┐
│ YOUR MACHINE │
│ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ Spring Boot PaaS App │ │ PostgreSQL (port 5432) │ │
│ │ (port 8080) │────▶│ Stores projects & │ │
│ │ │ │ deployment records │ │
│ │ REST API: │ └──────────────────────────┘ │
│ │ POST /api/projects │ │
│ │ POST /api/projects/ │ ┌──────────────────────────┐ │
│ │ {id}/deploy │────▶│ Docker Daemon │ │
│ │ GET /api/deployments │ │ Builds images & │ │
│ └──────────────────────────┘ │ runs containers │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌───────────────────────────────────────────┼───────────────┐ │
│ │ Docker Network: Paas-net │ │ │
│ │ │ │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────┴───────┐ │ │
│ │ │ Traefik │ │ App 1 │ │ App 2 │ │ │
│ │ │ port 80 │ │ app1.local │ │ app2.localhost │ │ │
│ │ │ (reverse │──│ host │ │ │ │ │
│ │ │ proxy) │ └────────────┘ └─────────────────┘ │ │
│ │ └────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
| Class | Purpose |
|---|---|
Project |
Represents a registered application. Stores the name, repositoryUrl, subdomain, containerId (active Docker container), and optional env vars (as a @ElementCollection map). |
Deployment |
Represents a single deployment attempt. Tracks the status (enum), logs (TEXT column for real-time build output), commitHash, containerId, and timestamps. Has a @ManyToOne relationship to Project. |
DeploymentStatus |
Enum defining the deployment lifecycle: PENDING → CLONING → BUILDING → DEPLOYING → RUNNING (or FAILED). |
How they relate:
Project 1 ──────< Deployment 1 (RUNNING)
Deployment 2 (FAILED)
Deployment 3 (PENDING)
One project can have many deployments. Each deployment goes through the status pipeline independently.
DeploymentController — The REST API layer. No business logic here, just HTTP ↔ Service wiring.
| Endpoint | Method | What It Does |
|---|---|---|
/api/projects |
POST |
Register a new project (name + GitHub URL) |
/api/projects |
GET |
List all registered projects |
/api/projects/{id} |
GET |
Get a specific project |
/api/projects/{id}/deploy |
POST |
Trigger a deployment — creates a Deployment record in PENDING, then kicks off the async pipeline |
/api/deployments/{id} |
GET |
Poll deployment status & logs |
/api/deployments |
GET |
List all deployments |
Key behavior: The deploy endpoint returns 202 Accepted immediately with the deployment record. The actual work happens asynchronously — the client polls GET /api/deployments/{id} to watch progress.
Orchestrationservice — The brain of the deployment pipeline. Annotated with @Async so it runs in a background thread.
Pipeline steps:
deployProject(deploymentId)
│
├── 1. VALIDATE → Check project has a repository URL
│
├── 2. CLONING → Call GitService.cloneRepository()
│ Clones repo to a temp directory
│
├── 3. BUILDING → Call DockerService.buildImage()
│ Builds Docker image from the repo's Dockerfile
│
├── 4. CLEANUP → Stop old container (if project was previously deployed)
│
├── 5. DEPLOYING → Call DockerService.createAndStartContainer()
│ Creates container with Traefik labels on Paas-net
│
└── 6. RUNNING → Update project's containerId, clean up temp files
Error handling: Any exception at any step catches into FAILED status with the full stack trace saved into the deployment's logs field.
Real-time logging: Each step calls appendLog() which saves a timestamped message to the database immediately, so you can poll GET /api/deployments/{id} to see progress.
| Class | Purpose |
|---|---|
GitService |
Interface — cloneRepository(String repoUrl) |
GitServiceImpl |
Implementation using JGit library |
Smart URL handling:
- Browser URLs like
https://github.com/user/repo/tree/mainare automatically normalized tohttps://github.com/user/repo.git - Branch is extracted from the URL (e.g.,
/tree/develop→ clonesdevelopbranch) - 60-second clone timeout to prevent hanging on unreachable repos
Clone flow:
Input URL → extractBranch() → normalizeGitUrl() → JGit clone to temp dir → return File
DockerServiceImpl — Wraps the docker-java client library.
| Method | What It Does |
|---|---|
buildImage(dir, tag) |
Pings Docker daemon first (fail-fast check), verifies Dockerfile exists, builds with streaming log output, 5-minute timeout |
createAndStartContainer(imageId, appName) |
Creates container named Paas-<appName> on Paas-net network with Traefik routing labels |
stopContainer(containerId) |
Stops and removes a container |
Traefik labels applied to each container:
traefik.enable = true
traefik.http.routers.<appName>.rule = Host(`<appName>.localhost`)
traefik.http.services.<appName>.loadbalancer.server.port = 8080
This means the deployed app must listen on port 8080 inside its container.
| Class | Purpose |
|---|---|
DockerConfig |
Creates the DockerClient bean using default Docker host (connects to Docker Desktop) |
SecurityConfig |
Disables CSRF and permits all requests (no auth — development mode) |
StartupCleanup |
Runs on app startup — finds any deployments stuck in PENDING/CLONING/BUILDING/DEPLOYING and marks them FAILED. Prevents stale deployments from previous crashes. |
| Interface | Purpose |
|---|---|
projectrepo |
JPA repository for Project — standard CRUD |
deploymentrepo |
JPA repository for Deployment — standard CRUD + findByStatusIn(List<DeploymentStatus>) for startup cleanup |
Runs Traefik as a Docker container:
- Listens on port 80 for HTTP traffic
- Dashboard available at port 8081
- Watches Docker events to auto-discover containers with Traefik labels
- Must be on the same
Paas-netnetwork as deployed containers
- Java 25 (as specified in
build.gradle) - Docker Desktop — must be running
- PostgreSQL — running on
localhost:5432with databasepostgres - Docker network — create once:
docker network create Paas-net
cd PaasDemo
docker compose up -d./gradlew.bat bootRunThe API will be available at http://localhost:8080
curl -X POST http://localhost:8080/api/projects \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "repositoryUrl": "https://github.com/username/repo"}'Important: The repository must contain a
Dockerfileat its root, and the app inside must listen on port 8080.
curl -X POST http://localhost:8080/api/projects/1/deploycurl http://localhost:8080/api/deployments/1Watch the status field progress: PENDING → CLONING → BUILDING → DEPLOYING → RUNNING
The logs field shows real-time output from each step.
Once status is RUNNING, open:
http://my-app.localhost
(Replace my-app with whatever project name you used in step 3)
| Method | Endpoint | Body | Response |
|---|---|---|---|
POST |
/api/projects |
{"name": "...", "repositoryUrl": "..."} |
201 + Project JSON |
GET |
/api/projects |
— | 200 + List of Projects |
GET |
/api/projects/{id} |
— | 200 + Project JSON or 404 |
| Method | Endpoint | Body | Response |
|---|---|---|---|
POST |
/api/projects/{id}/deploy |
— | 202 + Deployment JSON (PENDING) |
GET |
/api/deployments/{id} |
— | 200 + Deployment JSON with status & logs |
GET |
/api/deployments |
— | 200 + List of all Deployments |
PENDING ──► CLONING ──► BUILDING ──► DEPLOYING ──► RUNNING
│ │ │ │
└────────────┴────────────┴────────────┴──► FAILED (on any error)
Each status transition is persisted to the database immediately, so polling GET /api/deployments/{id} gives real-time progress.
PaasDemo/
├── compose.yaml # Traefik reverse proxy
├── build.gradle # Dependencies & config
├── Dockerfile # (For containerizing PaaS itself)
└── src/main/java/com/sujal/PaasDemo/
├── PaasDemoApplication.java # Entry point (@EnableAsync)
├── Models/
│ ├── Project.java # Project entity
│ ├── Deployment.java # Deployment entity
│ └── DeploymentStatus.java # Status enum
├── controller/
│ └── DeploymentController.java # REST API
├── Services/
│ ├── Orchestration/
│ │ └── Orchestrationservice.java # Async deployment pipeline
│ ├── git/
│ │ ├── GitService.java # Interface
│ │ └── GitServiceImpl.java # JGit clone implementation
│ └── docker/
│ └── DockerServiceImpl.java # Docker build & run
├── config/
│ ├── DockerConfig.java # DockerClient bean
│ ├── SecurityConfig.java # Permit all (dev mode)
│ └── StartupCleanup.java # Clean stale deployments
└── repo/
├── projectrepo.java # Project JPA repo
└── deploymentrepo.java # Deployment JPA repo
| Problem | Cause | Solution |
|---|---|---|
Deployment stuck at BUILDING |
Docker Desktop not running, or build hangs | The app now pings Docker before building and has a 5-min timeout. Restart the app to auto-mark stale deployments as FAILED. |
502 Bad Gateway at *.localhost |
App inside container not listening on port 8080 | Ensure your Dockerfile configures the app to listen on port 8080 |
404 at *.localhost |
Traefik can't find a matching route | Check container is on Paas-net network and has correct Traefik labels |
Connection refused at *.localhost |
Traefik not running | Run docker compose up -d in the PaasDemo directory |
Port 8080 conflict on docker compose up |
Spring Boot already uses 8080 | Traefik dashboard is mapped to 8081 to avoid this |