Deploying Next.js on Hop3¶
This guide walks you through deploying a Next.js application on Hop3. By the end, you'll have a production-ready React application with server-side rendering running on your own infrastructure.
Prerequisites¶
Before you begin, ensure you have:
- A Hop3 server - Follow the Installation Guide if you haven't set one up yet
- The Hop3 CLI - Installed on your local machine
- Node.js 18+ - Install from nodejs.org or via your package manager
- npm - Comes with Node.js
- Git - For version control and deployment
Verify your local setup:
Step 1: Create a New Next.js Application¶
Create a new Next.js app with minimal configuration:
CI=true NEXT_TELEMETRY_DISABLED=1 npx --yes create-next-app@latest hop3-tuto-nextjs --yes --typescript --tailwind --eslint --app --src-dir --no-import-alias --use-npm --no-turbopack
Verify the project structure:
Step 2: Configure for Production Deployment¶
Enable Standalone Output¶
Create next.config.mjs to enable standalone output mode (creates a minimal production bundle).
This will replace any existing Next.js config:
rm -f next.config.ts next.config.mjs next.config.js 2>/dev/null; ls -la next.config* 2>/dev/null || echo "Config files removed"
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
images: {
unoptimized: process.env.NEXT_IMAGE_UNOPTIMIZED === "true",
},
};
export default nextConfig;
Verify the config file was created:
Create Health Check Endpoint¶
Create an API route for health checks:
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
status: "ok",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
});
}
Create a simple /up endpoint:
import { NextResponse } from "next/server";
export async function GET() {
return new NextResponse("OK", { status: 200 });
}
Step 3: Customize the Home Page¶
Replace the default home page with a custom welcome page:
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Hello from Hop3!</h1>
<p className="text-xl text-gray-600 mb-8">
Your Next.js application is running.
</p>
<div className="flex gap-4 justify-center">
<a
href="/api/health"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Health Check
</a>
</div>
</div>
</main>
);
}
Step 4: Build and Verify¶
Build the application to ensure it compiles correctly:
Verify the standalone output was created:
Step 5: Create Deployment Configuration¶
Create a Procfile¶
Create a Procfile in your project root:
# Pre-build: Install dependencies and build
prebuild: npm install && npm run build
# Copy static files to standalone directory
prerun: cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/ 2>/dev/null || true
# Main web process (uses standalone server)
web: node .next/standalone/server.js
Create hop3.toml¶
Create a hop3.toml for advanced configuration:
[metadata]
id = "hop3-tuto-nextjs"
version = "1.0.0"
title = "My Next.js Application"
[build]
before-build = [
"npm install",
"npm run build"
]
packages = ["nodejs", "npm"]
[run]
start = "node .next/standalone/server.js"
before-run = "cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/ 2>/dev/null || true"
[env]
NODE_ENV = "production"
HOSTNAME = "0.0.0.0"
[port]
web = 3000
[healthcheck]
path = "/up"
timeout = 30
interval = 60
Verify the deployment files exist:
Step 6: Initialize Git Repository¶
Create a .gitignore file (Next.js creates one, but let's ensure it's complete):
# Dependencies
node_modules/
/.pnp
.pnp.js
# Build output
/.next/
/out/
/build
# Environment
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Vercel
.vercel
# TypeScript
*.tsbuildinfo
next-env.d.ts
# OS files
.DS_Store
Thumbs.db
# IDE
.idea/
.vscode/
*.swp
*.swo
Initialize the repository:
Step 7: Deploy to Hop3¶
The following steps require a Hop3 server. Set the HOP3_SERVER environment variable to your server address before running these commands.
Configure the CLI¶
If this is your first deployment, initialize Hop3:
Deploy¶
Deploy the application (first deployment creates the app):
Set Hostname¶
Configure the hostname for nginx proxy:
Wait for Process Stop¶
Wait for the previous deployment to fully stop:
Apply Configuration¶
Redeploy to apply the hostname configuration:
You'll see output showing:
- Code upload
- Dependency installation (npm install)
- Application build (npm run build)
- Static file copying
- Application startup
Step 8: Verify Deployment¶
Check your application status:
Managing Your Application¶
Restart the Application¶
View and Manage Environment Variables¶
# List all variables
hop3 config:show hop3-tuto-nextjs
# Set a variable
hop3 config:set hop3-tuto-nextjs NEXT_PUBLIC_API_URL=https://api.example.com
# Remove a variable
hop3 config:unset hop3-tuto-nextjs OLD_VARIABLE
Scaling¶
# Check current processes
hop3 ps hop3-tuto-nextjs
# Scale web workers
hop3 ps:scale hop3-tuto-nextjs web=2
Advanced Configuration¶
Adding a Database (PostgreSQL with Prisma)¶
Install Prisma:
Configure the database in prisma/schema.prisma:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
createdAt DateTime @default(now())
}
Update hop3.toml to run migrations:
Attach a database addon:
hop3 addons:create postgres hop3-tuto-nextjs-db
hop3 addons:attach hop3-tuto-nextjs hop3-tuto-nextjs-db
Environment Variables for Client-Side¶
Next.js exposes environment variables prefixed with NEXT_PUBLIC_ to the browser:
# Server-side only (secure)
hop3 config:set hop3-tuto-nextjs DATABASE_URL=postgres://...
hop3 config:set hop3-tuto-nextjs SECRET_KEY=your-secret
# Client-side (visible in browser)
hop3 config:set hop3-tuto-nextjs NEXT_PUBLIC_API_URL=https://api.example.com
Security Note
Never put secrets in NEXT_PUBLIC_ variables. They are embedded in the JavaScript bundle and visible to anyone.
Custom Server (Advanced)¶
For custom server needs, create server.js:
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const hostname = process.env.HOSTNAME || 'localhost';
const port = parseInt(process.env.PORT || '3000', 10);
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();
app.prepare().then(() => {
createServer(async (req, res) => {
try {
const parsedUrl = parse(req.url, true);
await handle(req, res, parsedUrl);
} catch (err) {
console.error('Error occurred handling', req.url, err);
res.statusCode = 500;
res.end('internal server error');
}
}).listen(port, () => {
console.log(`> Ready on http://${hostname}:${port}`);
});
});
Update Procfile:
Image Optimization¶
Next.js Image Optimization requires additional configuration for self-hosting:
// next.config.ts
const nextConfig: NextConfig = {
output: "standalone",
images: {
// Use unoptimized images (simplest)
unoptimized: true,
// Or configure remote patterns for external images
// remotePatterns: [
// { protocol: 'https', hostname: 'example.com' }
// ],
},
};
ISR (Incremental Static Regeneration)¶
ISR works out of the box with standalone mode. Configure revalidation in your pages:
// In a Server Component
export const revalidate = 60; // Revalidate every 60 seconds
export default async function Page() {
const data = await fetch('https://api.example.com/data');
return <div>{/* ... */}</div>;
}
API Routes with Database¶
Example API route with database access:
// src/app/api/users/route.ts
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export async function GET() {
const users = await prisma.user.findMany();
return NextResponse.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const user = await prisma.user.create({
data: { email: body.email, name: body.name },
});
return NextResponse.json(user, { status: 201 });
}
Troubleshooting¶
Application Won't Start¶
Check the logs for errors:
Common issues:
- Missing standalone output: Ensure output: "standalone" is in next.config.ts
- Static files not copied: Check the prerun command in Procfile
- Wrong hostname binding: Set HOSTNAME=0.0.0.0
Build Failures¶
If the build fails:
- Check Node.js version matches locally and on server
- Ensure all dependencies are in
dependencies(not justdevDependencies) - Run
npm run buildlocally to see detailed errors
Memory Issues¶
Next.js can consume significant memory during build:
Static Assets Not Loading¶
Ensure static files are copied to standalone directory:
Slow Cold Starts¶
To improve cold start times: - Minimize dependencies - Use dynamic imports for heavy components - Consider edge runtime for API routes where appropriate
Static Export (Alternative)¶
For fully static sites (no SSR), use static export:
Update deployment:
[build]
before-build = ["npm install", "npm run build"]
[run]
start = "npx serve out -l $PORT"
[env]
NODE_ENV = "production"
Install serve:
Next Steps¶
- CLI Reference - Complete command reference
- hop3.toml Reference - Full configuration options
- Backup and Restore Guide - Protect your data
Example Files¶
Complete hop3.toml for Next.js¶
# hop3.toml - Next.js Application
[metadata]
id = "hop3-tuto-nextjs"
version = "1.0.0"
title = "My Next.js Application"
author = "Your Name <you@example.com>"
[build]
before-build = [
"npm install",
"npm run build"
]
packages = ["nodejs"]
[run]
start = "node .next/standalone/server.js"
before-run = "cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/ 2>/dev/null || true"
[env]
NODE_ENV = "production"
HOSTNAME = "0.0.0.0"
[port]
web = 3000
[healthcheck]
path = "/up"
timeout = 30
interval = 60
[[provider]]
name = "postgres"
plan = "standard"
Complete Procfile for Next.js¶
# Procfile - Next.js Application
# Build phase
prebuild: npm install && npm run build
# Pre-run: Copy static assets to standalone directory
prerun: cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/ 2>/dev/null || true
# Web server (standalone mode)
web: node .next/standalone/server.js