Entorno de desarrollo local con Next.js, Drizzle, Nginx y PostgreSQL


Configuración entorno de desarrollo web con Next.JS, tendremos dos contenedores Docker, uno con Nginx y otro con PostgreSQL, la aplicación Next.js estará en un directorio del SO.
Creamos un certificado auto firmado para tener conexión segura(SSL), entre cliente navegador y servidor Nginx, Nginx hace de proxy entre cliente y aplicación, de este modo en desarrollo utilizamos cookies seguras, etc, nos acercamos en el modelo de desarrollo al de producción.
En docker-compose.yml configuramos una red, así tenemos la IP que no cambia en reinicios, etc, del contenedor, al configurar una red, como se ve más abajo en el fichero docker-compose.yml.
Utilizamos drizzle-kit, simplificara la interacción con la base de datos PostgreSQL y facilitara la migración, actualización de la estructura por cambios en el modelo de la aplicacion, entre otros.
El Setup del entorno de desarrollo local lo realizamos con un script en Node.js, creara el fichero .env, con las variables de entorno necesarias y los dos contenedores Docker.
Permitir usuarios no root ejecutar el contenedor con Docker Engine, añadir el usuario al grupo docker
$ sudo usermod -aG docker usuario
Creamos
$ npx create-next-app@latest app && npm i drizzle-orm dotenv drizzle-kit postgres tsx promisify
$ cd app && mkdir -p lib/db lib/db/migrations
$ mkdir -p data/nginx/conf.d data/nginx/ssl
Creamos certificado auto firmado
$ cd app/data/nginx/ssl && openssl req -newkey rsa:4096 -x509 -nodes -days 3560 -out nextjs.crt -keyout nextjs.key
Fichero: app/data/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /config/nginx/conf.d/*.conf;
}
Fichero: app/lib/db/setup.ts
import { exec } from 'node:child_process';
import { promises as fs } from 'node:fs';
import { promisify } from 'node:util';
import readline from 'node:readline';
import crypto from 'node:crypto';
import path from 'node:path';
const execAsync = promisify(exec);
function question(query: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) =>
rl.question(query, (ans) => {
rl.close();
resolve(ans);
})
);
}
async function getPostgresURL(): Promise<string> {
console.log('Step 1: Setting up Postgres');
const dbChoice = await question(
'Do you want to use a local Postgres instance with Docker (L) or a remote Postgres instance (R)? (L/R): '
);
if (dbChoice.toLowerCase() === 'l') {
console.log('Setting up local Postgres instance with Docker...');
await setupLocalPostgres();
return 'postgres://next_saas:1234@localhost:54322/next_saas';
} else {
console.log(
'You can find Postgres databases at: https://vercel.com/marketplace?category=databases'
);
return await question('Enter your POSTGRES_URL: ');
}
}
async function setupLocalPostgres() {
console.log('Checking if Docker is installed...');
try {
await execAsync('docker --version');
console.log('Docker is installed.');
} catch (error) {
console.error(
'Docker is not installed. Please install Docker and try again.'
);
console.log(
'To install Docker, visit: https://docs.docker.com/get-docker/'
);
process.exit(1);
}
console.log('Creating docker-compose.yml file...');
const dockerComposeContent = `
services:
postgres:
image: postgres:latest
container_name: next_saas_starter_postgres
environment:
POSTGRES_DB: next_saas
POSTGRES_USER: next_saas
POSTGRES_PASSWORD: 1234
ports:
- "54322:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
dev_net:
ipv4_address: 10.8.0.2
nginx:
image: nginx:latest
container_name: next_saas_starter_nginx
ports:
- '80:80'
- '443:443'
environment:
- TZ=Europe/Madrid
- PUID=1000
- PGID=1000
volumes:
- ./data:/config
- ./letsencrypt:/etc/letsencrypt
- ./data/nginx/nginx.conf:/etc/nginx/nginx.conf
networks:
dev_net:
ipv4_address: 10.8.0.3
volumes:
postgres_data:
networks:
dev_net:
driver: bridge
ipam:
config:
- subnet: 10.8.0.0/16
gateway: 10.8.0.1
`;
await fs.writeFile(
path.join(process.cwd(), 'docker-compose.yml'),
dockerComposeContent
);
console.log('docker-compose.yml file created.');
console.log('Starting Docker container with docker compose up -d ...');
try {
await execAsync('docker compose up -d');
console.log('Docker container started successfully.');
} catch (error) {
console.error(
'Failed to start Docker container. Please check your Docker installation and try again.'
);
process.exit(1);
}
}
function generateAuthSecret(): string {
console.log('Step 2: Generating AUTH_SECRET...');
return crypto.randomBytes(32).toString('hex');
}
async function writeEnvFile(envVars: Record<string, string>) {
console.log('Step 3: Writing environment variables to .env');
const envContent = Object.entries(envVars)
.map(([key, value]) => ${key}=${value})
.join('
');
await fs.writeFile(path.join(process.cwd(), '.env'), envContent);
console.log('.env file created with the necessary variables.');
}
async function main() {
//await checkStripeCLI();
const POSTGRES_URL = await getPostgresURL();
const BASE_URL = 'http://localhost:3000';
const AUTH_SECRET = generateAuthSecret();
await writeEnvFile({
POSTGRES_URL,
BASE_URL,
AUTH_SECRET,
});
console.log('🎉 Setup completed successfully!');
}
main().catch(console.error);
Fichero: app/lib/db/schema.ts
import {
pgTable,
serial,
varchar,
text,
timestamp,
integer,
boolean,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
firstname: varchar('firstname', { length: 100 }),
lastname: varchar('lastname', { length: 100 }),
email: varchar('email', { length: 255 }).notNull().unique(),
passwordHash: text('password_hash').notNull(),
role: varchar('role', { length: 20 }).notNull().default('member'),
confirmed: boolean('confirmed').notNull().default(false),
otpSecret: varchar('otp_secret', { length: 16 }).notNull().default('TSVK5IUB7E4RSTV5'),
created: timestamp('created').notNull().defaultNow(),
updated: timestamp('updated').notNull().defaultNow(),
deleted: timestamp('deleted'),
});
export const categorys = pgTable('categorys', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull().unique(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
brief: varchar('brief', { length: 500 }).notNull(),
content: text('content').notNull(),
user_id: integer('user_id')
.notNull()
.references(() => users.id),
category_id: integer('category_id')
.notNull()
.references(() => categorys.id),
});
// Users - One-to-Many - Post
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const categoryRelations = relations(categorys, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
user: one(users, {
fields: [posts.user_id],
references: [users.id],
}),
category: one(categorys, {
fields: [posts.category_id],
references: [categorys.id],
}),
}));
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Category = typeof categorys.$inferSelect;
export type NewCategory = typeof categorys.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
export type CategoryDataWithPosts = Category & {
posts: (Post & {
user: Pick<User, 'id' | 'firstname' | 'email'>;
})[];
};
Fichero: app/drizzle.config.ts
import type { Config } from 'drizzle-kit';
import dotenv from 'dotenv'
dotenv.config({ path: "./.env" });
export default {
schema: './lib/db/schema.ts',
out: './lib/db/migrations',
dialect: "postgresql",
dbCredentials: { url: process.env.POSTGRES_URL!, },
} satisfies Config;
Fichero: app/package.json
...
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"db:setup": "npx tsx lib/db/setup.ts",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
},
...
$ npm run db:setup && docker ps
$ docker exec -it next_saas_starter_postgres bash
$ su - postgres -c 'psql'
postgres=# create user next_saas login encrypted password '1234';
postgres=# create database next_saas with owner next_saas;
postgres=# exit
$ cd app && npm run db:generate && npm run db:migrate
Creamos el virtual host
Fichero: app/data/nginx/conf.d/default.conf
server {
listen 80;
listen [::]:80;
listen 443 ssl;
server_name localhost;
server_tokens off;
ssl_certificate /config/nginx/ssl/nextjs.crt;
ssl_certificate_key /config/nginx/ssl/nextjs.key;
ssl_protocols TLSv1.3;
#ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA HIGH !RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS";
#ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
#access_log /var/log/nginx/host.access.log main;
location / {
proxy_pass http://10.8.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
$ docker exec -it next_saas_starter_nginx bash
service nginx restart
exit
cd app && npm run dev
Related Posts
Dockerizar una aplicación Next.JS con Nginx y PostgreSQL
2025-09-21 00:00:00

Cifrar JSON Web Token con Flask
2025-03-17 00:00:00

Web blog Next.js 15
2025-11-23 00:00:00
Popular Category
Subscribe to receive future updates
Lorem ipsum dolor sited Sed ullam corper consectur adipiscing Mae ornare massa quis lectus.
No spam guaranteed, So please don’t send any spam mail.