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 DatabaseKeeping 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:
Push code to GitHub.
Build the Docker image using GitHub Actions.
Push the image to a container registry.
Pull the prebuilt image onto the VPS.
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 psThe 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-secretNever 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-streamIf 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 -hAlso 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.
The conversation
Sign in to like this story or join the conversation.
There are no approved comments yet. Be the first to add a perspective.