How to Deploy a Next.js Application on a $5 VPS Using Docker

Deploying a modern Next.js application doesn't always require expensive cloud infrastructure. For a small website, personal project, or early-stage product, an affordable virtual private server (VPS) can be a practical starting point.

In this guide, we'll deploy a Next.js application using Docker, configure automatic HTTPS with Caddy, and discuss how to run the application on a server with limited memory.

The goal isn't to promise enterprise-scale performance for $5 a month. It's to build a simple, understandable deployment that can grow as your traffic increases.

1. Understanding the Architecture

Our deployment uses four main components:

  • Next.js: Renders the website and handles server-side application logic.

  • Docker: Packages the application with its runtime dependencies.

  • Caddy: Acts as a reverse proxy and manages HTTPS certificates.

  • PostgreSQL: Stores application data, hosted externally to conserve server memory.

The request flow is straightforward:

Visitor
   |
   v
Domain / DNS
   |
   v
Caddy (HTTPS)
   |
   v
Next.js Docker Container
   |
   v
External PostgreSQL Database

Keeping PostgreSQL outside a 512 MB server is especially important. Even if average traffic is low, the operating system, web server, Node.js process, and database can compete for scarce memory.

2. Why Use Docker?

Docker makes deployments more predictable.

Instead of installing Node.js packages directly on the production server, we build an application image containing everything needed to run the website.

This provides several advantages:

  • More consistent development and production environments.

  • Simpler application upgrades.

  • Easier rollback to a previous image.

  • Isolation between the application and reverse proxy.

  • Less configuration drift over time.

For a very small VPS, the key is to build the image somewhere else and run only the finished image on the server.

3. Configure Next.js for Standalone Output

Next.js supports standalone output, which produces a smaller runtime bundle containing the files needed to serve the application.

Add this to next.config.ts:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
};

export default nextConfig;

This is particularly useful for Docker deployments because the runtime container doesn't need the entire development dependency tree.

4. Create a Multi-Stage Dockerfile

A multi-stage build separates application compilation from runtime execution.

Here is a simplified example for a Next.js application:

FROM node:22-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:22-alpine AS runner

WORKDIR /app

ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000

CMD ["node", "server.js"]

This example assumes the project contains a public directory and doesn't need additional native runtime libraries. Applications using Prisma or other native dependencies may require extra build steps and packages.

In a real production project, also run the process as a non-root user and ensure sensitive files are excluded from the Docker build context.

5. Use Docker Compose

Docker Compose makes it easier to manage multiple services.

Create compose.yml:

services:
  app:
    image: ghcr.io/your-user/your-app:latest
    restart: unless-stopped
    env_file:
      - .env
    expose:
      - "3000"

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    environment:
      DOMAIN: ${DOMAIN}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

The application port is exposed only to other containers on Docker's internal network. Caddy handles public HTTP and HTTPS traffic.

For more repeatable production deployments, use an immutable version tag rather than relying indefinitely on latest.

6. Configure Automatic HTTPS with Caddy

Create a Caddyfile:

{$DOMAIN} {
    encode gzip
    reverse_proxy app:3000
}

Then configure your DNS record to point your domain to the server's public IP address.

Ensure inbound ports 80 and 443 are open in both your cloud firewall and operating system firewall.

Once the DNS record resolves correctly, Caddy can request and renew HTTPS certificates automatically.

This avoids manually managing certificate renewal scripts.

7. Build Outside the Production Server

A 512 MB VPS is not a suitable environment for compiling a modern Next.js application.

The build process may involve:

  • TypeScript processing.

  • JavaScript bundling.

  • Server-side compilation.

  • Database client generation.

  • Static asset optimization.

Together, these can require substantially more memory than the final application runtime.

A better workflow is:

  1. Push code to GitHub.

  2. Build the Docker image using GitHub Actions.

  3. Push the image to a container registry.

  4. Pull the prebuilt image onto the VPS.

  5. Restart only the application container.

On the VPS, a deployment can then be as simple as:

docker compose pull app

docker compose up -d \
  --no-deps app

docker compose ps

The exact workflow depends on how your registry authentication and CI pipeline are configured.

8. Configure Environment Variables Safely

Production applications commonly require database credentials, OAuth secrets, and API keys.

Store these as runtime environment variables rather than hardcoding them in source code.

For example:

NODE_ENV=production

DATABASE_URL=postgresql://...

NEXTAUTH_URL=https://example.com

NEXTAUTH_SECRET=your-secure-secret

Never commit real secrets to GitHub.

For Next.js, remember that NEXT_PUBLIC_* variables are usually embedded into browser bundles at build time. Changing them may require rebuilding your Docker image.

Server-only secrets should remain outside the frontend bundle.

9. Optimize for a Small VPS

Small servers require careful resource management.

Monitor memory consumption

Check memory usage with:

free -h
docker stats --no-stream

If Node.js consistently uses most of the available RAM, the application may become unstable during traffic spikes.

Use swap carefully

Swap can provide emergency memory headroom, but it isn't a replacement for physical RAM. Heavy swapping can cause significant latency.

Keep the database external

Using a managed PostgreSQL service reduces local resource usage, but introduces network latency and service-specific usage limits.

Enable caching where appropriate

Static assets and public images are good candidates for CDN caching.

For personalized, authenticated, or rapidly changing content, choose caching behavior carefully to avoid exposing private information or serving stale data.

Avoid unnecessary background services

Do not add Redis, analytics workers, or other persistent processes unless the application actually needs them.

10. Monitor the Deployment

A successful HTTP response doesn't guarantee that the entire application is healthy.

Useful checks include:

docker compose ps
docker compose logs --tail=100 app
df -h
free -h

Also monitor:

  • Application response times.

  • Container restart counts.

  • Database connectivity.

  • Disk usage.

  • Error rates.

  • HTTPS certificate status.

For a public website, configure off-server backups and test restoration before relying on the deployment for important data.

11. Know When to Upgrade

A 512 MB VPS can be a useful experiment, but it has limited headroom.

Consider upgrading when you observe persistent memory pressure, container restarts, excessive swap usage, unacceptable response times, or increased concurrent traffic.

Moving to 1 GB or 2 GB RAM may be more valuable than repeatedly tuning an application that has outgrown its server.

Measure real usage rather than making scaling decisions based only on requests per day.

Final Thoughts

An affordable VPS can be a practical hosting option for a small Next.js project.

The most important design decisions are to separate build and runtime workloads, keep resource-heavy services outside the smallest instance, use a reverse proxy for HTTPS, and monitor the system before traffic grows.

Start with simple infrastructure, understand its limits, and scale based on evidence.

A reliable deployment is ultimately more valuable than the cheapest possible server.