Deploy a Node.js or Next.js App to a VPS with InfraPilot
InfraPilot Team
March 8, 2026
When to Move Off Managed Platforms
Vercel and Netlify are excellent for deploying static sites and serverless functions. But once your app needs a persistent database, WebSocket connections, background job queues, or long-running processes, managed platforms become awkward and expensive fast. A VPS with Docker and InfraPilot gives you full control without the serverless constraints.
Containerise Your App
First, add a Dockerfile to your project. For a Next.js app:
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
For a plain Node.js/Express app, the Dockerfile is simpler:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "src/index.js"]
The Compose File
Create docker-compose.yml on your VPS:
services:
app:
image: ghcr.io/your-org/your-app:latest
restart: unless-stopped
environment:
DATABASE_URL: ${DATABASE_URL}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXTAUTH_URL: https://app.yourdomain.com
ports:
- "3000:3000"
depends_on:
- db
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
Set Up the Reverse Proxy and SSL
In InfraPilot, go to Traffic → Proxy Hosts → Add Proxy Host:
- Domain:
app.yourdomain.com - Forward host:
app, port:3000 - Enable WebSocket support if your app uses real-time features
- Enable Let's Encrypt SSL and force HTTPS
InfraPilot handles the Nginx config and certificate issuance automatically — no manual certbot or Nginx edits needed.
CI/CD: Auto-Deploy on Push
For automatic deployments, build and push to GitHub Container Registry in your CI pipeline:
# .github/workflows/deploy.yml
- name: Build and push
run: |
docker build -t ghcr.io/your-org/your-app:latest .
docker push ghcr.io/your-org/your-app:latest
- name: Deploy via SSH
run: |
ssh user@your-server "cd ~/apps/myapp && docker compose pull && docker compose up -d"
Monitor Your App in InfraPilot
Once deployed, InfraPilot gives you live container metrics, unified logs from the app and database, and alerting if either container restarts unexpectedly. The web terminal lets you run database migrations or debug sessions without leaving your browser.
Related posts
Blocking Vulnerable Images Before They Ship
Nightly vulnerability scans catch bad images after they are already running. Deploy gates move the check to build time, so a critical CVE blocks the deploy instead of becoming next month's incident.
Secrets in Self-Hosted Docker: Stop Committing .env Files
The .env file works until it leaks, drifts, or gets committed by accident. Here is what moving to an encrypted secrets store with versioning and container binding actually changes, and how to migrate without code changes.
Letting an AI Assistant Deploy for You, Safely
AI assistants are good at ops work, but you do not want a model wired straight to production. Preview-only API keys let an assistant build and tear down disposable environments while production stays off limits.