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

author
By admin
21 de septiembre de 2025
image

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

 

 
Share this post :

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.