DEV Community

Jose Francisco
Jose Francisco

Posted on

¿Cómo ejecutar Node.js, PostgreSQL en contenedores dentro de un entorno de desarrollo?

Si no quieres depender de algún entorno de desarrollo integrado (IDE) en la nube (GitHub Codespaces, Firebase Studio, Replit, etc) la clave es usar Docker, Docker Compose, Git, y un repositorio en la nube como GitHub, GitLab o Bitbucket.

Descripción general:

Este tutorial consiste en Dockerizar una aplicación Backend de Node.js y una base de datos de PostgresSQL, controlando todo el stack de la aplicación con un solo archivo. Los temas cubiertos incluye:

  • Ejecutar PostgresSQL, Adminer en un contenedor local.
  • Conterizar aplicación Node.js.
  • Configurar archivo Docker Compose y Dockerfile.

Definiciones.

  • Docker
    Dado que Docker está disponible y es compatible con Linux, macOS y Windows, la mayoría del software puede ser utilizado en cualquier computadora. Docker simplifica y flexibiliza el proceso de configuración de la aplicación, permitiéndole implementar, administrar y escalar bases de datos en contenedores aislados con solo unos pocos comandos.

  • Docker Compose
    Docker Compose simplifica el control de toda tu stack de aplicaciones, lo que facilita la gestión de servicios, redes y volúmenes en un único archivo de configuración YAML comprensible. Luego, con un solo comando, puedes crear y iniciar todos los servicios desde tu archivo de configuración.

  • Git
    Es un sistema de control de versiones que permite a los desarrolladores guardar diferentes versiones de archivos y rastrear los cambios realizados en un proyecto.

  • GitHub
    Es una plataforma basada en la nube que aloja repositorios de código y facilita la colaboración entre desarrolladores, permitiendo que trabajen en proyectos simultáneamente y mantengan un seguimiento de los cambios.

Requisitos:

  • Instalar Docker.
  • Instalar Docker Compose.
  • Instalar Node.js.
  • Instalar Git.
  • Una cuenta de GitHub.

Si clonas el repositorio de GitHub, ve al Paso 2 - Dockeriza la aplicación..

Paso 1 - Crea el proyecto local.

A continuación, se va a describir cómo crear la app con Node.js sin Docker, con una breve explicación de cada una de sus partes.

1. Crea el directorio del proyecto:

Pega los siguientes comandos en tu terminal:

mkdir members-only
cd  members-only

Enter fullscreen mode Exit fullscreen mode

2. Crea los archivos y directorios de la aplicación:

Pega los siguientes comandos en tu terminal:

mkdir src controllers db middleware public routes views
touch package.json
Enter fullscreen mode Exit fullscreen mode

3. Configurar tu package.json:

En el package.json pega lo siguiente:

{
  "name": "docker-compose",
  "version": "1.0.0",
  "private": true,
  "main": "src/app.js",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "scripts": {
    "start": "nodemon src/app.js",
    "deploy": "node src/app.js",
    "formatter": "semistandard --fix"
  },
  "dependencies": {
    "bcrypt": "5.1.1",
    "dotenv": "16.4.7",
    "ejs": "3.1.10",
    "express": "4.21.2",
    "express-session": "1.18.1",
    "express-validator": "7.2.1",
    "passport": "0.7.0",
    "passport-local": "1.0.0",
    "pg": "8.13.3"
  },
  "devDependencies": {
    "nodemon": "3.1.9",
    "semistandard": "17.0.0"
  },
  "eslintConfig": {
    "extends": [
      "./node_modules/semistandard/eslintrc.json"
    ]
  },
  "nodemonConfig": {
    "watch": [
      "src"
    ],
    "ignore": [
      "*.test.ts"
    ],
    "delay": "3",
    "execMap": {
      "ts": "ts-node"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

IMPORTANTE: Debido a que la aplicación se ejecutará dentro de un contenedor, te recomiendo utilizar nodemon en lugar de node --watch.

Este último presenta inestabilidad en contenedores, ya que solo detecta modificaciones en los archivos una única vez y deja de monitorearlos posteriormente.

Explicando los atributos principales del package.json:

Atributo Descripción
"main": "src/app.js" El archivo principal es src/app.js.
"start": "nodemon src/app.js" Corre la app con nodemon (modo desarrollo).
"formatter": "semistandard --fix" Formatea el código con semistandard.
“Dependencies” Son los paquetes que se van a usar en producción
“devDependencies”: Son los paquetes que solo se van a usar en desarrollo
nodemon Utilidad que monitorea cualquier cambio en el directorio fuente y reinicia automáticamente el servidor o la aplicación.

4. Configura app.js.

Crea el archivo app.js en el directorio src:

touch src/app.js
Enter fullscreen mode Exit fullscreen mode

Pega el siguiente código en src/app.js:

const path = require("node:path");
const express = require("express");

const session = require("express-session");
const passport = require("passport");

const routes = require("./routes/index.js")

const app = express();

app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");

app.use(express.static(path.join(__dirname, "public")));

app.use(session({ secret: "cats", resave: false, saveUninitialized: false }));
app.use(passport.session());
app.use(express.urlencoded({ extended: false }));

// routes
app.use(routes);

const PORT = process.env.PORT_NODE_SERVER ?? 3000;
const IP_NODE_SERVER = process.env.IP_NODE_SERVER ?? "localhost";

app.listen(PORT, () => {
  console.log(`http://${IP_NODE_SERVER}:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Este código configura la aplicación web con Express.js, establece el motor de plantillas EJS, gestiona sesiones y autenticación, y define las rutas de la aplicación.

Aun el proyecto no funcionara, falta crear la base de datos, las consultas, las rutas y las vistas.

5. Routes

Pega el siguiente código en routes/index.js:

const express = require("express");

const indexController = require("../controllers/indexController.js");
const loginInController = require("../controllers/logInController.js");
const logOutController = require("../controllers/logOutController.js");
const memberJoinController = require("../controllers/memberJoinController.js");
const signUpController = require("../controllers/signUpController.js");
const createMsgController = require("../controllers/createMsgController.js");

const middlewareSignUpValidation = require("../middleware/validation/signUpValidation.js");
const middlewareCreateMsgValidation = require("../middleware/validation/createMsgValidation.js");

const router = express.Router();

router.get("/", indexController.getIndex);
router.get("/log-in", loginInController.getLoginForm);
router.get("/sign-up", signUpController.getSignupForm);
router.get("/log-out", logOutController.getLogout);
router.get("/member-join", memberJoinController.getMemberJoinForm);
router.get("/create-msg", createMsgController.getCreateMsgForm );


router.post(
  "/sign-up",
  middlewareSignUpValidation.validateUser(),
  signUpController.create
);

router.post(
  "/log-in",
  loginInController.passportLocalStrategy.authenticate("local", {
    successRedirect: "/",
    failureRedirect: "/log-in",
  })
);

router.post("/member-join", memberJoinController.confirmSecretAccess);

router.post("/create-msg", middlewareCreateMsgValidation.validateMsg(), createMsgController.create);

router.all("*", (req, res) => {
  if (req.isAuthenticated()) {
    res.status(404).render("404");
  } else {
    res.redirect("/log-in");
  }
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Aquí es donde se organiza las rutas de la aplicación y asigna un controlador. Cuando el usuario accede a una ruta (por ejemplo, inicio, ingreso, registro o creación de mensajes), se invoca el controlador correspondiente para generar el formulario o la vista.

6. BD

En src/bd/pool.js pega el siguiente codigo:

const { Pool } = require("pg");

require("dotenv").config();

const pool = new Pool({
  host: process.env.HOSTS_DB, // or wherever the db is hosted
  port: process.env.PORT_DB,
  user: process.env.USER_DB,
  password: process.env.PASSWORD_DB,
  database: process.env.DATABASE_DB, // The default port
});

module.exports = pool;
Enter fullscreen mode Exit fullscreen mode

En src/db/query.js pega el siguiente código:

const pool = require("./pool.js");

async function getAllTable(table) {
  const { rows } = await pool.query(`SELECT * FROM ${table};`);
  return rows;
}

async function getAllmsg() {
  const { rows } = await pool.query(
    `SELECT u.username, m.date, m.description, m.title FROM logs_messages m INNER JOIN users u ON u.id = m.user_id;`
  );
  return rows;
}

async function getSecretCode(username) {
  const { rows } = await pool.query(
    `SELECT username FROM users WHERE username = '${username}';`
  );
  // console.log(rows);

  return rows.map((x) => x.username).toString();
}

async function updateStatusMember(id, secretCode) {
  const { rows } = await pool.query(
    `UPDATE users 
     SET is_member = true
     WHERE id = '${id}' AND username = '${secretCode}';`
  );

  return rows;
}

async function insertMsg (id, title, description) {
  const { rows } = await pool.query(
    "insert into logs_messages (user_id, title, description) values ($1, $2, $3)",
    [id, title, description]
  );

  return rows;
}

module.exports = {
  getAllTable,
  getAllmsg,
  getSecretCode,
  updateStatusMember,
  insertMsg,
};
Enter fullscreen mode Exit fullscreen mode
  • pool.js: Crea y exporta un pool de conexiones a PostgreSQL.

  • query.js: Ejecuta consultas SQL.

7. Controlladores

touch createMsgController.js logInController.js memberJoinController.js indexController.js logOutController.js signUpController.js
Enter fullscreen mode Exit fullscreen mode

En src/controllers/createMsgController.js pega el siguiente codigo:

const db = require("../db/query");
const bcrypt = require("bcrypt");

async function getCreateMsgForm(req, res) {
  if (req.isUnauthenticated()) {
    res.redirect("/");
  } else {
    res.render("pages/create-msg.ejs");
  }
}

async function create(req, res, next) {
  try {
    const insert = await db.insertMsg(
      req.user.id,
      req.body.title,
      req.body.description
    );

    insert

    res.redirect("/");
  } catch (error) {
    console.error(error);
    next(error);
  }
}

module.exports = { getCreateMsgForm, create };

Enter fullscreen mode Exit fullscreen mode

En src/controllers/logInController.js pega el siguiente codigo:

const db = require("../db/query.js");
const passportLocalStrategy = require("../middleware/passportLocalStrategy.js");

async function getLoginForm(req, res) {
  if (req.user !== undefined) res.redirect("/");
  const messages = await db.getAllmsg();
  if (req.isUnauthenticated()) {
    res.render("pages/log-in.ejs");
  } else {
    res.render("pages/index.ejs", { messages, user: req.user });
  }
}

function handleForm(req, res, next) {}

module.exports = { getLoginForm, handleForm, passportLocalStrategy };
Enter fullscreen mode Exit fullscreen mode

En src/controllers/memberJoinController.js pega el siguiente codigo:

const db = require("../db/query.js");
// const passportLocalStrategy = require("../middleware/passportLocalStrategy.js");

async function getMemberJoinForm(req, res) {

  if (req.isUnauthenticated() || req.user.is_member) {
    res.redirect("/");
  } else {
    res.render("pages/memberJoin.ejs");
  }

}

async function confirmSecretAccess(req, res) {
  try {
    const getSecret = await db.getSecretCode(req.body.secretCode);

    if (!req.user.is_member) {
      await db.updateStatusMember(req.user.id, req.body.secretCode);
    }

    if (req.body.secretCode !== getSecret) {
      return res.render("pages/memberJoin.ejs", {
        incorrect: "secret code incorrect!!!!",
      });
    }

    res.redirect("/");

  } catch (error) {
    console.error("Error in confirmSecretAccess:", error);
    res.status(500).send("Internal Server Error");
  }
}


module.exports = { getMemberJoinForm, confirmSecretAccess };
Enter fullscreen mode Exit fullscreen mode

En src/controllers/indexController.js pega el siguiente codigo:

const db = require("../db/query.js");

async function getIndex(req, res) {
  const messages = await db.getAllmsg();

  if (req.isUnauthenticated()) {
    res.render("pages/index.ejs", { messages });
  } else {
    // console.log(req)
    res.render("pages/index.ejs", { user: req.user, messages });
  }

}

module.exports = { getIndex };
Enter fullscreen mode Exit fullscreen mode

En src/controllers/logOutController.js pega el siguiente codigo:

async function getLogout(req, res, next) {
  req.logout((err) => {
    if (err) {
      return next(err);
    }
    res.redirect("/");
  });
}
module.exports = { getLogout }
Enter fullscreen mode Exit fullscreen mode

En src/controllers/signUpController.js pega el siguiente codigo:

const pool = require("../db/pool.js");
const bcrypt = require("bcrypt");

async function getSignupForm(req, res) {
  if (req.isUnauthenticated()) {
    res.render("pages/sign-up-form.ejs");
  } else {
    res.redirect("/");
  }
}

async function create(req, res, next) {
  try {
    const hashedPassword = await bcrypt.hash(req.body.password, 10);
    await pool.query(
      "insert into users (username, password, first_name, last_name) values ($1, $2, $3, $4)",
      [
        req.body.username,
        hashedPassword,
        req.body.first_name,
        req.body.last_name,

      ]
    );
    // AUTHENTICATE
    res.redirect("/log-in");
  } catch (error) {
    console.error(error);
    next(error);
  }


}

module.exports = { getSignupForm, create };
Enter fullscreen mode Exit fullscreen mode

8. Middleware

Pega los siguientes comandos en tu terminal:

mkidr src/middleware/validation
touch src/middleware/passportLocalStrategy.js  src/middleware/validation/createMsgValidation.js  src/middleware/validation/signUpValidation.js 
Enter fullscreen mode Exit fullscreen mode

En src/middleware/passportLocalStrategy.js pega el siguiente codigo:

const pool = require("../db/pool.js");
const passportLocalStrategy = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const bcrypt = require("bcrypt");

passportLocalStrategy.use(
  new LocalStrategy(async (username, password, done) => {
    try {
      const { rows } = await pool.query(
        "SELECT * FROM users WHERE username = $1",
        [username]
      );
      const user = rows[0];

      if (!user) {
        return done(null, false, { message: "Incorrect username" });
      }

      const match = await bcrypt.compare(password, user.password);
      if (!match) {
        // passwords do not match!
        return done(null, false, { message: "Incorrect password" });
      }

      // if (user.password !== password) {
      //   return done(null, false, { message: "Incorrect password" });
      // }
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  })
);

passportLocalStrategy.serializeUser((user, done) => {
  done(null, user.id);
});

passportLocalStrategy.deserializeUser(async (id, done) => {
  try {
    const { rows } = await pool.query("SELECT * FROM users WHERE id = $1", [
      id,
    ]);

    const user = rows[0];

    done(null, user);
  } catch (err) {
    done(err);
  }
});


module.exports = passportLocalStrategy;

Enter fullscreen mode Exit fullscreen mode

En src/middleware/validation/createMsgValidation.js pega el siguiente codigo:

const { body, oneOf, validationResult } = require("express-validator");
3

const leastOneErr =
  "At least one valid name field (title, description)  must be provided";

const validateMsg = () => {
  return [
    oneOf(
      [
        body("title")
          .trim() /*.isEmpty()*/
          .isLength({ max: 500 }),
        body("description")
          .trim() /*.isEmpty()*/
          .isLength({ max: 255 }),
      ],
      { message: `${leastOneErr}` }
    ),

    (req, res, next) => {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        const checkError = errors.array().map((error) => error.msg);

        res.status(400).json({
          msg: checkError,
        });

        return;
      }
      next();
    },
  ];
};

module.exports = { validateMsg };
Enter fullscreen mode Exit fullscreen mode

En src/middleware/validation/signUpValidation.js pega el siguiente codigo:

const { body, oneOf, validationResult } = require("express-validator");


const leastOneErr =
  "At least one valid name field (first_name, last_name, or username) must be provided";

const validateUser = () => {
  return [
    oneOf(
      [
        body("first_name").trim()/*.isEmpty()*/.isLength({ max: 50 }),
        body("last_name").trim()/*.isEmpty()*/.isLength({ max: 50 }),
        body("username").trim()/*.isEmpty()*/.isLength({ max: 255 }),
      ],
      { message: `${leastOneErr}` }
    ),
    body("password")
      .trim()
      // .isEmpty()
      .isLength({ max: 255 })
      .withMessage(`Password invalid`),
    body("passwordConfirmation")
      .trim()
      .custom((value, { req }) => {
        return value === req.body.password;
      })
      .withMessage(`password must match`),

    (req, res, next) => {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        const checkError = errors.array().map((error) => error.msg);

        res.status(400).json({
          msg: checkError,
        });

        return;
      }
      next();
    },
  ];
};

module.exports = { validateUser };

Enter fullscreen mode Exit fullscreen mode

9. Estilos y vistas

mkdir src/views/pages src/views/partials
touch src/views/pages/create-msg.ejs  src/views/pages/index.ejs   src/views/pages/log-in.ejs src/views/pages/memberJoin.ejs src/views/pages/sign-up-form.ejs src/views/partials/footer.ejs  src/views/partials/nav.ejs src/views/sign-up-form.ejs
Enter fullscreen mode Exit fullscreen mode

En src/views/pages/create-msg.ejs pega el siguiente codigo:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Create Messages</title>
  <link rel="stylesheet" href="css/base.css" />
  <link rel="stylesheet" href="css/pages/create-msg.css" />  
</head>

<body>
  <div class="container">
    <div class="login-card">

      <h1 class="title">Create Message</h1>

      <form action="/create-msg" method="POST">
        <div class="input-group">
          <label for="title">Title</label>
          <input id="title" name="title" placeholder="title" type="text" required />
        </div>
        <div class="input-group">
          <label for="description">Description</label>
          <textarea name="description" id="description" required></textarea>
        </div>
        <div class="btn-form">
          <button class="btn" type="submit">Send</button>
        </div>

      </form>

    </div>

  </div>

</body>

</html>
Enter fullscreen mode Exit fullscreen mode

En src/views/pages/index.ejs pega el siguiente codigo:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Members Only</title>
  <link rel="stylesheet" href="css/base.css" />
  <link rel="stylesheet" href="css/structure.css" />
  <link rel="stylesheet" href="css/pages/index.css" />
  <link rel="stylesheet" href="css/pages/container_message.css" />

</head>

<body class="grid-index">
  <header>
    <%- include("../partials/nav.ejs") %>
  </header>
  <main>
    <div class="title">
      <h1>Messages</h1>
      <% if (locals.user) { %>
         <a href="/create-msg">Create a new message</a> 
      <% } %>
    </div>
    <div class="container-msg">

        <% messages.forEach(item=> { %>
          <dl class="card">
            <% if (locals.user && locals.user.is_member != undefined && locals.user.is_member === true ) {%>
              <dt class="info-card-span author">
                <p>
                  <%= item.username %>
                </p>
                <p>
                  <%= item.date.toDateString() %>
                </p>
              </dt>

              <% } %>
                <dt class="info-card-span text">
                  <p>
                    <%= item.title %>
                  </p>
                </dt>
                <dt class="info-card-span text">
                  <p>
                    <%= item.description %>
                  </p>
                </dt>
          </dl>
          <% }) %>


    </div>
    <%# if (locals.user) {%>
      <%# } %>
  </main>
  <%# else { %>
    <!--   -->
    <%#}%>
      <%- include("../partials/footer.ejs") %>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

En src/views/pages/log-in.ejs pega el siguiente codigo:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Login</title>
  <link rel="stylesheet" href="css/base.css" />
  <link rel="stylesheet" href="css/pages/log-in.css" />  
</head>

<body>
  <div class="container">
    <div class="login-card">

      <h1 class="title">Log In</h1>

      <form action="/log-in" method="POST">
        <div class="input-group">
          <label for="username">Username</label>
          <input id="username" name="username" placeholder="username" type="text" required />
        </div>
        <div class="input-group">
          <label for="password">Password</label>
          <input id="password" name="password" placeholder="password" type="password" required />
        </div>
        <div class="btn-form">
          <button class="btn" type="submit">Log In</button>
        </div>
        <div class="or">or</div>
        <div class="sign-up-link">
          <p>Are You New? <a href="/sign-up">Create Account</a></p>
        </div>
      </form>

    </div>

  </div>

</body>

</html>
Enter fullscreen mode Exit fullscreen mode

En src/views/pages/memberJoin.ejs pega el siguiente codigo:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Login</title>
  <link rel="stylesheet" href="css/base.css" />
  <link rel="stylesheet" href="css/pages/log-in.css" />  
</head>

<body>
  <div class="container">
    <div class="login-card">

      <h1 class="title">Log In</h1>

      <form action="/log-in" method="POST">
        <div class="input-group">
          <label for="username">Username</label>
          <input id="username" name="username" placeholder="username" type="text" required />
        </div>
        <div class="input-group">
          <label for="password">Password</label>
          <input id="password" name="password" placeholder="password" type="password" required />
        </div>
        <div class="btn-form">
          <button class="btn" type="submit">Log In</button>
        </div>
        <div class="or">or</div>
        <div class="sign-up-link">
          <p>Are You New? <a href="/sign-up">Create Account</a></p>
        </div>
      </form>

    </div>

  </div>

</body>

</html>
Enter fullscreen mode Exit fullscreen mode

En src/views/pages/sign-up-form.ejs pega el siguiente código:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Sign Up</title>
  <link rel="stylesheet" href="css/base.css" />
  <link rel="stylesheet" href="css/pages/sign-up-form.css" />

</head>

<body>
  <div id="container">

    <div class="photo-container">
      <div class="logo">
        <img class="img-odin"
          src="https://cdn.statically.io/gh/TheOdinProject/curriculum/5f37d43908ef92499e95a9b90fc3cc291a95014c/html_css/project-sign-up-form/odin-lined.png"
          alt="odin">
        <p>Members Only</p>
      </div>
      <div class="text-logo">
        <p>Photo by
          <a href="https://unsplash.com/es/fotos/planta-de-hoja-verde-en-fotografia-de-primer-plano-25xggax4bSA">
            Halie West
          </a>
          on
          <a href="https://unsplash.com/es/fotos/planta-de-hoja-verde-en-fotografia-de-primer-plano-25xggax4bSA">
            Unsplash
          </a>
        </p>
      </div>

      <img src="images/halie-west-25xggax4bSA-unsplash.jpg" alt="img">

    </div>
    <div class="form-container">
      <div class="center">
        <form id="form-area" name="form" action="/sign-up" method="POST">
          <div class="title">
            <h1>Let's do this</h1>

          </div>
          <div class="input-group">
            <label for="first_name">First Name</label>
            <input id="first_name" name="first_name" placeholder="first_name" type="text" required />
          </div>
          <div class="input-group">
            <label for="last_name">Last Name</label>
            <input id="last_name" name="last_name" placeholder="last_name" type="text" required />
          </div>
          <div class="input-group">
            <label for="username">Username</label>
            <input id="username" name="username" placeholder="username" type="text" required />
          </div>
          <div class="input-group">
            <label for="password">Password</label>
            <input id="password" name="password" type="password" autocomplete="password"
              placeholder="password" aria-describedby="password" required>
          </div>
          <div class="input-group">

            <label for="passwordConfirmation">Confirm Password</label>
            <input id="passwordConfirmation" name="passwordConfirmation" type="password" aria-describedby="password confirmation"
              placeholder="password Confirmation" required>

            <div class="password-requirements">
              <p class="requirement hidden error" id="match">* Passwords must match</p>
            </div>
          </div>
          <div class="btn-form">
            <button class="btn" type="submit" id="signin">Sign Up</button>

          </div>


          <div class="or">or</div>
          <div class="login-link">
            <p>Already have an account? <a href="/log-in">Login</a></p>
          </div>


        </form>

      </div>

    </div>
  </div>

  <script src="js/sign-up-form.js"></script>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

En src/views/partials/footer.ejs pega el siguiente código:

<footer>
  <p>Copyright © github.com/Jose-C0 2025</p>
  <a href="https://github.com/Jose-C0" target="_blank">
    <img src="images/github-mark.png" alt="https://github.com/Jose-C0" />
  </a>
</footer>
Enter fullscreen mode Exit fullscreen mode

En src/views/partials/nav.ejs pega el siguiente código:

<nav>
  <div class="container-nav">
    <ul class="left-links">
      <li><a href="/member-join"><img class="img-odin"
        src="https://cdn.statically.io/gh/TheOdinProject/curriculum/5f37d43908ef92499e95a9b90fc3cc291a95014c/html_css/project-sign-up-form/odin-lined.png"
        alt="odin"></a></li>
      <li><a href="/">Clubhouse</a></li>
    </ul>
    <ul class="right-links">
      <% if(locals.user) { %> 
        <li><p> 
          WELCOME BACK <%= locals.user.username %>
        </p>
        </li>
        <li><a href="/log-out"> Log-out </a></li>
      <% } else { %>
        <li><a href="/log-in"> Log-in </a></li>
        <li><a href="/sign-up"> Sign-up </a></li>
      <% } %>

    </ul>
  </div>
</nav>
Enter fullscreen mode Exit fullscreen mode

En src/views/sign-up-form.ejs pega el siguiente código:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title></title>
    <link rel="stylesheet" href="css/base.css" />
</head>

<body>
    <h1>Sign Up</h1>
    <form action="" method="POST">
        <label for="username">Username</label>
        <input id="username" name="username" placeholder="username" type="text" />
        <label for="password">Password</label>
        <input id="password" name="password" type="password" />
        <button>Sign Up</button>
    </form>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

Así debería verse tu carpeta src:

src
├── app.js
├── controllers
├── db
├── middleware
├── public
├── routes
└── views

Como no tenemos base de datos la aplicación lanzará un error. Haremos eso enseguida

Paso 2 - Dockeriza la aplicación.

Tanto la app como el contenedor están diseñados para funcionar con las variables de entorno. Esto, sumado a que aún no tienes base de datos, hace que sigan apareciendo errores al correr el comando npm start.

1. Crea las variables de entorno:

En la raiz del projecto crea el archivo .env.

touch .env
Enter fullscreen mode Exit fullscreen mode

Pega las variables de entorno en tu archivo .env:

HOSTS_DB="localhost"
PORT_DB="5432"
USER_DB="odin"
PASSWORD_DB="secreto"
DATABASE_DB="odindb"
IP_NODE_SERVER="localhost"
PORT_NODE_SERVER="1234"
PORT_NODE_SERVER_DOCKER="1234"
Enter fullscreen mode Exit fullscreen mode

2. Crea un archivo Dockerfile:

En la raíz del proyecto crea el archivo Dockerfile:

touch Dockerfile
Enter fullscreen mode Exit fullscreen mode

Pega el siguiente código en Dockerfile:

ARG NODE_VERSION=20.13.1

FROM node:${NODE_VERSION}-alpine

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

CMD npm start
Enter fullscreen mode Exit fullscreen mode

El Dockerfile se encarga de:

  • Usar la versión 20.13.1 de Node.js sobre Alpine Linux.
  • Define /app como directorio de trabajo dentro del contenedor.
  • Copia package.json y ejecuta npm install para instalar dependencias.
  • Copia el resto de los archivos del proyecto (solo la carpeta src) al contenedor.
  • Ejecuta npm start como comando principal al iniciar el contenedor.

3. Crea un archivo compose.yaml:

Crea el archivo compose.yaml en la raíz del proyecto:

touch  compose.yaml
Enter fullscreen mode Exit fullscreen mode

Pega el código siguiente en compose.yaml:

 services:
  app:
    container_name: nodejs-authentication-basics
    build: .
    env_file:
      - path: ./.env
    ports:
      - "${PORT_NODE_SERVER}:1234"
    develop:
      watch:
        - action: sync
          path: .
          target: /app
          ignore:
            - node_modules/
        - action: rebuild
          path: package.json
    depends_on:
      - postgres_server_test_odin
    networks:
      - app-network

  postgres_server_test_odin:
    container_name: postgres_server_test_odin
    image: postgres:17.1 
    #build: ./scripts/
    #healthcheck:
    #  test: ["CMD", "bash", "-c", "./dbIsEmpty.sh"]
    #  start_period: 10s
    env_file:
      - path: ./.env
    environment:
      # POSTGRES_HOST_AUTH_METHOD: ${HOSTS_DB}
      POSTGRES_SERVER_PORT: ${PORT_DB}
      POSTGRES_USER: ${USER_DB}
      POSTGRES_PASSWORD: ${PASSWORD_DB}
      POSTGRES_DB: ${DATABASE_DB}
    ports:
      - ${PORT_DB}:${PORT_DB}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - app-network

  adminer:
    image: adminer
    ports:
      - 8080:8080
    environment:
      ADMINER_DEFAULT_SERVER: postgres_server_test_odin
    depends_on:
      - postgres_server_test_odin
    networks:
      - app-network

volumes:
  postgres-data:

networks:
  app-network:
Enter fullscreen mode Exit fullscreen mode

Este docker-compose.yml define tres servicios conectados entre si.

Image description

  1. app. Contenedor de Node.js.

  2. postgres_server_test_odin. Contenedor del servidor postgresSQL.

  3. adminer. Contenedor del gestor de base de datos.

Image description

  • networks, es para conectar los contenedores a una misma red. En este caso usamos la red "app-network".

  • volumes, es para asegurar la persistencia de los datos incluso cuando el contenedor es eliminado. En este caso, la información de la base de datos es lo que queremos preservar.

Descripción del servicio app.

Image description

Atributo Descripción
container_name Define el nombre del contenedor
build Especifica el contexto de construcción para el servicio. En este caso, el contexto es el directorio actual (.)
env_file Especifica el archivo de variables de entorno que se utilizará. En este caso, el archivo es .env ubicado en el directorio .
ports Define las reglas de mapeo de puertos entre el host y el contenedor. En este caso, se mapea el puerto ${PORT_NODE_SERVER} del host al puerto 1234 del contenedor
develop Configuración de desarrollo, que incluye acciones como sync y rebuild
watch Lista de directorios y archivos a monitorear para cambios durante el desarrollo
action Acción a realizar cuando se detectan cambios en los archivos o directorios especificados
path Directorio o archivo a monitorear
target Directorio en el contenedor donde se sincronizarán los cambios
ignore Lista de directorios o archivos a ignorar durante la sincronización
depends_on Especifica los servicios en los que este servicio depende. En este caso, depende de postgres_server_test_odin
networks Define las redes a las que el contenedor se conectará. En este caso, se conecta a la red app-network

Descripción del servicio ** postgres_server_test_odin**.

Image description

Atributo Descripción
container_name Define el nombre del contenedor. En este caso, el nombre es postgres_server_test_odin.
image Especifica la imagen Docker a usar para el contenedor. En este caso, se usa la imagen postgres:17.1.
build Contexto de construcción del servicio. Comentado en este caso, pero si se descomenta, el contexto sería el directorio ./scripts/.
healthcheck Configuración del healthcheck para el contenedor. Comentado en este caso, pero si se descomenta, ejecutaría el script dbIsEmpty.sh para verificar la salud del contenedor.
env_file Especifica el archivo de variables de entorno que se utilizará. En este caso, el archivo es .env ubicado en el directorio ..
environment Define variables de entorno para el contenedor.
POSTGRES_SERVER_PORT Puerto del servidor PostgreSQL. Se toma del archivo de variables de entorno con la clave ${PORT_DB}.
POSTGRES_USER Usuario de PostgreSQL. Se toma del archivo de variables de entorno con la clave ${USER_DB}.
POSTGRES_PASSWORD Contraseña del usuario de PostgreSQL. Se toma del archivo de variables de entorno con la clave ${PASSWORD_DB}.
POSTGRES_DB Nombre de la base de datos de PostgreSQL. Se toma del archivo de variables de entorno con la clave ${DATABASE_DB}.
ports Define las reglas de mapeo de puertos entre el host y el contenedor. En este caso, se mapea el puerto ${PORT_DB} del host al puerto ${PORT_DB} del contenedor.
volumes Define los volúmenes a montar en el contenedor. En este caso, el volumen postgres-data se monta en /var/lib/postgresql/data.
networks Define las redes a las que el contenedor se conectará. En este caso, se conecta a la red app-network.

Descripción del servicio adminer.

Image description

Atributo Descripción
image Especifica la imagen Docker a usar para el contenedor. En este caso, se usa la imagen adminer.
ports Define las reglas de mapeo de puertos entre el host y el contenedor. En este caso, se mapea el puerto 8080 del host al puerto 8080 del contenedor.
environment Define variables de entorno para el contenedor.
ADMINER_DEFAULT_SERVER Especifica el servidor de base de datos predeterminado para Adminer. En este caso, el servidor es postgres_server_test_odin.
depends_on Especifica los servicios en los que este servicio depende. En este caso, depende de postgres_server_test_odin.
networks Define las redes a las que el contenedor se conectará. En este caso, se conecta a la red app-network.

4. Ejecutar app:

En la raíz del proyecto donde se encuentra el archivo compose.yaml, ejecuta el comando siguiente para iniciar los servicios del contenedor:

docker compose up –-watch
Enter fullscreen mode Exit fullscreen mode

Así debe verse tu terminal:

Image description
Nota: Presiona ctrl + c para detener los servicios del contenedor.

Si escribes localhost:8080 el gestor de base de datos debería aparecer.
Image description

Top comments (0)