When Your Frontend Needs a Server: Building a CI/CD Pipeline for SSR Applications

You've just finished a feature on your Next.js app. The build passes locally. You push to production. But instead of a working page, users see a blank screen with a spinning loader. The server process started, but it wasn't actually ready to handle requests.

This is the moment you realize that deploying a server-rendered frontend is fundamentally different from deploying a static site. The pipeline that worked for your React SPA or static Gatsby site won't cut it anymore.

The Core Difference: You're Deploying a Server, Not Files

With a static frontend, your build produces HTML, CSS, and JavaScript files. You upload them to a CDN or storage bucket, and you're done. The deployment is essentially a file copy operation.

With Server-Side Rendering (SSR), the build output includes server-side code that must run as a process. Frameworks like Next.js, Nuxt, or Remix generate a folder containing:

  • JavaScript code that runs on the server
  • JavaScript bundles for the client
  • Static assets like images and fonts
  • An entry point file (often server.js) that starts the application

Your pipeline now needs to treat this output as an application that runs continuously, not a set of files to serve. This changes everything about how you build, test, and deploy.

Step 1: Build with the Right Target

The build step looks similar to a static frontend at first glance. You run npm run build or the framework's equivalent command. But the output is different, and so is what you do with it.

For SSR, the build output must be packaged into something that can run on a server. If you're using containers, this means creating a Docker image that includes:

  • The built server code
  • Runtime dependencies (Node.js version, system libraries)
  • Any configuration files needed at runtime
  • The entry point script

Your Dockerfile might look something like this:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY .next ./.next
COPY public ./public
EXPOSE 3000
CMD ["node", ".next/standalone/server.js"]

The key detail: you're not copying the entire source code. You're copying only what's needed to run the application. This keeps the image small and reduces attack surface.

Step 2: Health Checks Are Not Optional

Here's where many SSR pipelines fail. The container starts, the process runs, and everyone assumes the application is working. But "process is running" is not the same as "application can serve requests."

Your application might start successfully but fail to render pages because:

  • A database connection times out
  • An external API is unreachable
  • Environment variables are missing
  • A required service isn't ready yet

Add a health check endpoint to your application. Usually this lives at /health or /api/health and returns a 200 status when the app can actually handle requests. Your pipeline must call this endpoint after deployment, before routing traffic to the new version.

If the health check fails, stop the pipeline. Don't let users see error pages or infinite loading states. The team investigates, fixes the issue, and runs the pipeline again.

Step 3: Choose Your Deployment Strategy

You have two common paths for deploying SSR applications: directly to a server or into containers. Each has different implications for your pipeline.

The following flowchart illustrates the complete SSR pipeline, from build to deployment, with the critical health check decision point.

flowchart TD A[Build SSR App] --> B[Package Artifact] B --> C[Deploy to Server/Cluster] C --> D[Run Health Check] D --> E{Health Check Passes?} E -->|Yes| F[Route Traffic to New Version] E -->|No| G[Stop Pipeline & Alert] F --> H[Track Running Version] G --> I[Investigate & Fix] I --> A H --> J[Monitor & Log]

Direct Server Deployment

You copy the build output to a server, stop the old process, and start the new one. The critical concern here is how you handle the transition.

If you kill the old process immediately, any requests currently being processed will fail. Users see errors mid-action. The solution is graceful shutdown: the old server stops accepting new requests but finishes processing the ones already in flight. Once those complete, the process exits cleanly. Then the new server starts and begins accepting traffic.

Your pipeline script needs to coordinate this handoff. It's doable but requires careful scripting and monitoring.

Container Deployment

Containers give you more control. The pipeline builds a new Docker image, pushes it to a registry, and deploys it to your container orchestration platform.

Here is a minimal Dockerfile that packages the built SSR application for container deployment:

FROM node:20-alpine

WORKDIR /app

# Copy production dependencies
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Copy built server and client assets
COPY .next ./.next
COPY public ./public

# Expose the port the app listens on
EXPOSE 3000

# Start the server
CMD ["node", ".next/standalone/server.js"]

With Kubernetes, this becomes a rolling update:

  1. A new pod starts with the new image
  2. The pod runs its health check
  3. Once healthy, traffic gradually shifts to the new pod
  4. Old pods are terminated after they finish their current requests

Kubernetes handles graceful shutdown and traffic shifting automatically. Your pipeline just needs to update the deployment manifest with the new image tag and apply it.

Step 4: Track What's Running

After deployment, your pipeline should record which version is running. Store the commit hash, image tag, or deployment timestamp somewhere accessible. This information is invaluable when you need to:

  • Roll back to a previous version
  • Investigate which deployment introduced a bug
  • Correlate performance issues with specific releases

A simple approach: tag your Docker images with the commit hash and store the mapping in a database or a simple text file. Your monitoring tools can then reference this data when alerting.

Practical Checklist for Your SSR Pipeline

Before you call your pipeline production-ready, verify these points:

  • Build output is packaged into a deployable artifact (Docker image or server package)
  • Health check endpoint exists and returns meaningful status
  • Pipeline waits for health check to pass before routing traffic
  • Pipeline stops and alerts if health check fails
  • Graceful shutdown is configured (old process finishes in-flight requests)
  • Rolling update strategy is tested (no downtime during deployment)
  • Version information is stored and accessible after deployment
  • Rollback process is documented and tested

The Takeaway

An SSR frontend is not a static site. It's a server application that happens to render HTML. Treat your pipeline accordingly: build for runtime, verify with health checks, deploy with zero-downtime strategies, and always know which version is serving your users. When you get these fundamentals right, your users never see the blank screen. They just see a fast, working page.