Scaling Your Next.js App: Integrating PostgreSQL and Docker for Production
Moving from a prototype with hardcoded data to a production-ready application requires a robust data strategy and a reliable deployment pipeline. In this tutorial, we’ll take our Next.js Fruit Store and replace its static data with a real PostgreSQL database, and then containerize the entire setup using Docker.
Why Move to PostgreSQL?
While hardcoded data or local JSON files are great for prototyping, they don’t scale. PostgreSQL offers:
- Persistence: Your data stays safe even if the application restarts.
- ACID Compliance: Ensures data integrity and reliable transactions.
- Performance: Optimized for complex queries and high concurrency.
Setting Up PostgreSQL
We’ll use the pg library, which is the standard, low-level PostgreSQL client for Node.js. It’s fast, reliable, and doesn’t hide the SQL behind complex abstractions.
1. Install Dependencies
2. Configure the Connection Pool
In a Next.js environment, especially during development with hot-reloading, it’s crucial to manage database connections properly. We’ll use a connection pool to avoid exhausting database resources.
Creating the Schema
Before we can use the database, we need to create our tables. We’ll use a simple seed script to initialize our fruit catalog.
We use NUMERIC(10,2) for the price to ensure decimal precision, which is critical for financial data.
Fetching Data with Server Components
One of the most powerful features of Next.js is Server Components. They allow us to fetch data directly from the database on the server, reducing the amount of JavaScript sent to the client and eliminating the need for client-side API calls.
The Query Layer
Rendering the Page
Our page.tsx becomes an async component. It fetches the data during server-side rendering, ensuring the user sees the content immediately.
tsx // src/app/page.tsx import { getFruits } from “@/lib/fruits”;
export default async function Home() { const fruits = await getFruits();
return ( <main className="container mx-auto p-4"> <h1 className="text-3xl font-bold mb-6">Fruit Store</h1> <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> {fruits.map(fruit => ( <FruitCard key={fruit.id} fruit={fruit} /> ))} </div> </main> ); }
Containerization with Docker
To ensure our application runs consistently across different environments, we’ll use Docker. By setting output: "standalone" in next.config.ts, Next.js creates a minimal production build that only includes the necessary files.
Our Dockerfile uses a multi-stage build to keep the image size small and secure:
- Deps: Installs only the necessary dependencies.
- Builder: Builds the application.
- Runner: The final, lightweight production image.
Deploying to the Cloud
With our Docker image ready, we can deploy to a service like Azure Container Apps. This provides a serverless environment that scales automatically based on traffic, allowing you to focus on building features rather than managing infrastructure.
Conclusion
Moving to a real database and containerizing your application are major steps toward production readiness. You now have a system that is persistent, scalable, and portable.
The journey doesn’t end here. Next, you might consider adding authentication, search functionality, or more advanced caching strategies.
Happy coding!