Try โ€‚โ€‰HackMD
tags: docker

Use Typescript + Docker to create a full-stake web application

It is the simple blueprint of what we are going to build over this article

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

Local Frontend express application

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

Get backend db working with official mongodb container

To make thing easier, we will need two containers

  1. mongodb: this is the official container of mongodb database
  2. mongo-express: this is the express ui to let us to manipulate the db easier.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

docker pull mongo
docker pull mongo-express

Now we have both mongodb & mongo-express images

Mongo Network

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

We can use docker network in which our application containers interact with each other by its container name

// create a new mongo network
docker network create mongo-network
// than we can check the newly created network
docker network ls

Run mongoDB & mongdb-express container

Mongodb

We can use docker run to cmd to run the container. Before that, we will need to define some environment varables. We can refer to the official doc to see the detailed variables.
We are just going to define the most important two:

MONGO_INITDB_ROOT_USERNAME
MONGO_INITDB_ROOT_PASSWORD

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

// name is container name, it is important becase we will need the name to connect to mongo-express
docker run -d \
    --net mongo-network \
    --name mongodb \
    -e MONGO_INITDB_ROOT_USERNAME=admin \
    -e MONGO_INITDB_ROOT_PASSWORD=admin \
    -p 27017:27017 \
    mongo

Now we should see our mongodb container runing by checking docker ps

Mongo-express

We can also see the detailed info in the official doc

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

There is a issue in newest version of mongo-express container setting. we need to use following env variable instead of the one defined in official doc. See the issue here

docker run -d\
    --name mongo-express \
    --net mongo-network \
    -p 8081:8081 \
    -e ME_CONFIG_MONGODB_URL=mongodb://admin:admin@mongodb:27017/ \
    mongo-express

then we can check if it is running good by docker logs <contianer id>

Server is open to allow connections from anyone (0.0.0.0)
basicAuth credentials are "admin:pass", it is recommended you change this in your config.js!

When we see the logs above, and we now are able to access the mongo-express by the following url:

http://localhost:8181

Connect mongodb with our local express app

How we have two docker containers runing and we can also connect the mongodb by mongo-express ui contianer thorugh our browser.

Now we need to, on the other hand, connect the mongodb with our frontend app that we just created.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

We will need to create a new database called "user-account". Then create one collection called "users". And we are ready to go.
Our data object format is like below:

{
    _id: ObjectId('6252a0ded62f5286e3f7187d'),
    userid: 1,
    name: 'eric lee',
    email: 'Colorlife@gmail.com',
    interests: 'code and life'
}

In our server code, we add the logic to connect the database.

import express from "express";
import path from "path";
import fs from "fs";
import { MongoClient } from "mongodb";
import bodyParser from "body-parser";
let app = express();

// Render static (CSS & JS) files from root folder
app.use(express.static(__dirname));

app.use(
  bodyParser.urlencoded({
    extended: true,
  })
);
app.use(bodyParser.json());

app.get("/", function (req, res) {
  res.sendFile(path.join(__dirname, "index.html"));
});

app.get("/profile-picture", function (req, res) {
  let img = fs.readFileSync(path.join(__dirname, "images/profile-1.jpg"));
  res.writeHead(200, { "Content-Type": "image/jpg" });
  res.end(img, "binary");
});

let mongoUrlLocal = "mongodb://admin:admin@localhost:27017";

// use when starting application as docker container
let mongoUrlDocker = "mongodb://admin:admin@mongodb";

// "user-account" in demo with docker. "my-db" in demo with docker-compose
let databaseName = "user-account";

app.post("/update-profile", function (req, res) {
  let userObj = req.body;
  try {
    MongoClient.connect(
      mongoUrlLocal,
      // mongoClientOptions,
      (err, client) => {
        if (client) {
          let db = client.db(databaseName);
          userObj["userid"] = 1;

          let query = { userid: 1 };
          let newvalues = userObj;
          let collection = db?.collection("users");
          collection.updateOne(query, { $set: newvalues }, (err, result) => {
            if (!err) {
              console.log(result);
              client.close();
            }
          });
        }
      }
    );

    res.status(200);
    res.send(userObj);
  } catch (e: any) {
    res.status(404);
    res.send(`Error connecting database: ${JSON.stringify(e)}`);
  }
  res.send("WIP: update-profile");
});

app.get("/get-profile", function (req, res) {
  // Connect to the db
  try {
    MongoClient.connect(mongoUrlLocal, async (err, client) => {
      let db = client?.db(databaseName);
      let query = { userid: 1 };
      let collection = db?.collection("users");
      let result = await collection?.findOne(query);
      res.send(result);
    });
  } catch (e) {
    res.status(404);
    res.send(`Error connecting database: ${JSON.stringify(e)}`);
  }
});

app.listen(3000, function () {
  console.log("app listening on port 3000!");
});

In our app.ts code, we also add some logic for querying mongodb database


async function handleUpdateProfileRequest() {
  try {
    const contEdit = document.getElementById("container-edit")!;
    const cont = document.getElementById("container")!;

    const payload = {
      name: (document.getElementById("input-name")! as HTMLInputElement).value,
      email: (document.getElementById("input-email")! as HTMLInputElement)
        .value,
      interests: (
        document.getElementById("input-interests")! as HTMLInputElement
      ).value,
    };

    const response = await fetch("http://localhost:3000/update-profile", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });
    const jsonResponse = await response.json();

    document.getElementById("name")!.textContent = jsonResponse.name;
    document.getElementById("email")!.textContent = jsonResponse.email;
    document.getElementById("interests")!.textContent = jsonResponse.interests;

    cont.style.display = "block";
    contEdit.style.display = "none";
  } catch (e) {
    console.log(e);
  }
}

function updateProfile() {
  const contEdit = document.getElementById("container-edit")!;
  const cont = document.getElementById("container")!;

  (document.getElementById("input-name")! as HTMLInputElement).value =
    document.getElementById("name")!.textContent ?? "";
  (document.getElementById("input-email")! as HTMLInputElement).value =
    document.getElementById("email")!.textContent ?? "";
  (document.getElementById("input-interests")! as HTMLInputElement).value =
    document.getElementById("interests")!.textContent ?? "";

  cont.style.display = "none";
  contEdit.style.display = "block";
}

(async function init() {
  const response = await fetch("http://localhost:3000/get-profile");
  const user = await response.json();
  document.getElementById("name")!.textContent = user.name
    ? user.name
    : "ColorfulLife.jp";
  document.getElementById("email")!.textContent = user.email
    ? user.email
    : "colorfulLife@example.com";
  document.getElementById("interests")!.textContent = user.interests
    ? user.interests
    : "coding, enjoying life";

  const cont = document.getElementById("container")!;
  cont.style.display = "block";
})();

Now we can see our data updated from database already.

Docker compose

Instead of docker run all the related contianers and create the network manually, docker-compose makes thing much easier.
We can run simply one command to activate all needed containers inside one docker network just simply create one docker compose config file.

version: "3.8"
services:
  mongodb:
    image: mongo
    ports:
      - 27017:27017
    environment:
      - MONGO_INITDB_ROOT_USERNAME=admin
      - MONGO_INITDB_ROOT_PASSWORD=admin
  mongo-express:
    image: mongo-express
    restart: always # fixes MongoNetworkError when mongodb is not ready when mongo-express starts
    ports:
      - 8080:8081
    environment:
      - ME_CONFIG_MONGODB_URL=mongodb://admin:admin@mongodb:27017/

You mignt noice that we do not define "name" & "net" becase docker will handle itself.

then, run following command and everything working properly:

// start up all containers
docker-compose -f docker-compose.yaml up -d
// stop all containers
docker-compose -f docker-compose.yaml down

merge our application with those two contianer to build a new docker image.

Now to make things more easy, we can make our own application as a sigle docker compainer by docker build command. Before that, we will need to create a Dockerfile

FROM node:13-alpine

ENV MONGO_DB_USERNAME=admin \
    MONGO_DB_PWD=admin

# The RUN command will be run inside the container, not host
RUN mkdir -p /home/app

# The COPY command can copy host content to docker container
COPY ./app /home/app

# set default dir so that next commands executes in /home/app dir
WORKDIR /home/app

# will execute npm install in /home/app because of WORKDIR
RUN npm install

RUN npm run build

# no need for /home/app/server.js because of WORKDIR
CMD ["npm", "run", "start"]

There is one more thing need to remember is that when we use this application as a container, it is run inside the docker network instead of local network. So, we will need to change the mongodb connection code insdie our server.ts code to make sure it only makes use of the name of the container

// use when starting application as docker container
let mongoUrlDocker = "mongodb://admin:admin@mongodb";

Then we can run the following command to build our own docker image

docker build . -t my-app

Now our application also becomes a contianer, so we can include it into our docker compose file.

version: "3.8"
services:
  my-app:
    image: my-app
    restart: always
    ports:
      - 3000:3000
  mongodb:
    image: mongo
    restart: always
    ports:
      - 27017:27017
    environment:
      - MONGO_INITDB_ROOT_USERNAME=admin
      - MONGO_INITDB_ROOT_PASSWORD=admin
  mongo-express:
    image: mongo-express
    restart: always # fixes MongoNetworkError when mongodb is not ready when mongo-express starts
    ports:
      - 8080:8081
    environment:
      - ME_CONFIG_MONGODB_URL=mongodb://admin:admin@mongodb:27017/

Finally, we only need to run one command to start our application

docker-compose -f docker-compose.yaml up -d

The drawing below is what our application now looks like:

See all the sample code from my git repo here