Nowadays, using Docker with Microservices is common practice, as it brings isolation, portability, scalability, and more. It will be a surprising for a backend engineer / full-stack engineer not to familiar with Docker (of course, combined with Kubernetes). In the article, I will introduce the core concept of Docker from the practical perspective, whether you are a college student who are interested in leraning Docker or a junior software developer just beginning your career in the software industry, you should be able to grasp the essentils of Docker that you need in practice in 10 minutes by reading the article.

The workflow with Docker in industrial projects

I found the following chart that perfectly explain the workflow with Docker in practice Docker Tutorial for Beginners [FULL COURSE in 3 Hours].

Trulli
Fig.1 - The Workflow with Docker

As shown above, the steps of the workflow are (all Docker related terms will be explained in the next section):

  • After you complete the application development and commit the code to Git, Jenkins will be notified via webhooks
  • Jenkins will buid the docker images according to the dockerfile provide
  • Jenkins will push the built images to the Docker repository, like Docker Hub or AWS ECR (Elastic Container Registry)
  • The server will pull the images from the Docker repository, then create and run containers according to the Docker Compose files (*compose.yaml files)

The Core Concepts of Docker

In the above workflow, I mentioned many essetianls in Docker. In this section, I will explain them in details.

Docker Sever (Docker Deamon)

It’s responsible for managing Docker containers, images, networks, and volumes. It is run on the same machine where Docker is installed. The Docker Client will send all requests to Docker Server.

Docker Client (CLI)

The Docker Client is the interface that users interact with when using Docker.

Docker Hub

Docker Hub is a Docker Repository. It is the default public registry for Docker images, created and maintained by Docker, Inc.

Docker Container

An instance of an image is called a container. It is the running image.

Docker Image

Stopped container. Docker image are read-only layers, and the docker container is the writable layer on top of a image.

Docker Compose

Docker Compose is a powerful tool for defining and managing multi-container Docker applicaitons. In practice, you can sepcify your applicaton’s services, networks, and volumes in a docker-compose.yml file, and then use a single command to start
them all.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version: '3.9'

services:
mongodb:
image: mongo:6.0 # Replace with the desired MongoDB version
container_name: mongodb-container
restart: always
ports:
- '27017:27017' # Expose MongoDB's default port
environment:
MONGO_INITDB_ROOT_USERNAME: admin # MongoDB root username
MONGO_INITDB_ROOT_PASSWORD: password # MongoDB root password
volumes:
- mongodb_data:/data/db # Persistent data storage

volumes:
mongodb_data:
driver: local

Dockerfile

A Dockerfile is a script that contains a series of instructions to build a Docker image, inccluding the base image,
dependencies, configurations, and commands to run the application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Use the official Node.js LTS image as the base image
FROM node:18-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy package.json and package-lock.json files to the container
COPY package*.json ./

# Install dependencies
RUN npm install --production

# Copy the rest of the application files
COPY . .

# Expose the port the app runs on
EXPOSE 3000

# Define the command to run the application
CMD ["npm", "start"]

About CMD ["npm", "start"]: Specifies the command to start the Node.js application. Make sure your package.json has a start script defined (e.g., “start”: “node index.js”).

Private Docker Registry

Normally, we push our Docker images to pricate docker registry rather than a public one. A commonly used private Docker registry is AWS ERC (Elastic Container Registry).

Docker Volumes

Docker Volumes is used to persist data in Docker and prevent data loss in stateful applications. They allow folders from the host’ physical file system (e.g. /home/mount/data) to be mounted into the virtual file system (e.g. /var/lib/mysql/data) of Docker.

There are three types of Docker volumes:

  • Docker-managed volumes: Managed entirely by Docker and stored in its default location.
  • Anonymous volumes: Automatically created without a specific name, typically for temporary use.
  • Named volumes: Explicitly named and can be referenced by their names. Named volumes are the most common choice in production environments due to their reusability and ease of management.

Summary

The above covers the core concepts of Docker, providing a quick start for beginners. You can explore each part in greater details based on your interests.

Comment and share

There are tons of articles online that offer tutorials about how to use Promise.all. I will not reiterate those. In this article, I aim to brief summarise the use of Promise.all in real-world projects for those programmers transitioning from junior to mid-level.

What do we need to keep in mind when considering using Promise.all

  • Order matters. Promise.all returns the results in the same order as the source.
  • Fail one, Fail all. If one of the promise rejects, then the Promise.all will not wait for the remaining promise to execute but rejects right away.

When do we need Promise.all

In real project, normally when we have an array of data, for each element in the array, we need to do some async/await operations, this is where we need Promise.all. For example, in the following code snippets from a real project, we have an array called idsForDelete, for each id in this array, we need to call an async method removeMonitorTask(apiKey, id). Therefore, we use Promise.all to do async/await calls on all ids in the array.

1
2
3
4
5
6
7
8
...
const deleteIds = await Promise.all(
idsForDelete?.map(async (id) => {
const response = await removeMonitorTask(apiKey, id);
return response?.data[0]?.trackerId?.toString() === id.toString();
})
...
);

How do we handle exceptions with Promise.all

There are two ways of handling exceptions with Promise.all:

  • catch an exception at the end of Promise.all;
  • catch an exception in each individual promise.

Only catching exceptions in the latter way when we need to:

  • Keep the results of other promise
  • Want to know the reason for each rejected promise

Check the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p1 = Promise.resolve(10);
p2 = Promise.resolve(20);
p3 = new Promise(function (resolve, reject) {
setTimeout(reject, 100, 'test');
}).catch((err) => {
console.log(`internal err: ${err}`);
});

(async () => {
const res = await Promise.all([p2, p3, p1]);
console.log(res);
})().catch((err) => {
console.error(`err: ${err}`);
});

Execution result:

1
2
3
-- internal err: test
-- [ 20, undefined, 10 ]

As we can see from the execution result that even p3 is rejected, we still get the results of previous p1 and the p2 afterwards.

What if we only catch the exception after Promise.all?

1
2
3
4
5
6
7
8
9
10
11
12
p1 = Promise.resolve(10);
p2 = Promise.resolve(20);
p3 = new Promise(function (resolve, reject) {
setTimeout(reject, 100, 'test');
});

(async () => {
const res = await Promise.all([p2, p3, p1]);
console.log(res);
})().catch((err) => {
console.error(`err: ${err}`);
});

Result:

1
-- err: test

As shown above, Promise.all rejects all, we are not able to see the result of p2.

That is it, that is all you need to know about Promise.all in most of the time when developing real-world projects.

Comment and share

Last week I was working on a task of adding multiple language support to the React Native app that is under developing. I used the package i18next as the backhone to support the feature. React Redux is also used to store the language choice as one of the app settings. During the development, I encountered an unexpected bug: The home screen does not re-render once I changed the language setting. It took me around 2.5 hours to resolve it, hence, I consider it worth a blog to write the think process and the solution down.

Context of the Issue

First, let us go through the context of the issue.

Store the Language Settings in React Redux

In the i18next, it has a global variable i18n.language to store language choice and is applied to the whole app. However, it is not enough, if we do not store the language setting, once the app is closed, the user choice will lost. Therefore, I used React Redux to store the language setting, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
const initialState = {
language: '',
}

export const appSettingSlice = createSlice({
name: 'appSettings',
initialState,
reducers: {
getLanguageSetting: (state) => {
return state.language
},
setLanguageSetting: (state, action) => {
if (!action.payload) return
state.language = action.payload
},
resetLanguageSetting: (state) => {
state.language = ''
},
},
})
...

The Expected Scenario

The project is structured as blow. The language in redux is set under set-language/index.jsx, along with i18n.language. And set-language is routed from my-account. What I expected is that if I change the language, once I switch back to the home screen, it should re-render and present with the changed language.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- root
-- app
-- (tabs)
-- home
...
-- index.jsx
...
-- my-account
...
-- set-language
-- index.jsx
-- index.style.js
...
-- index.js // entry
-- _layout.js
...
...
-- index.js
...

The Re-render Issue

Things did no go as I expected. Once I change the language setting and switch back to the home screen, it changes nothing, the scren still show the preivous language.

RCA (Root Cause Analysis)

I added a useEffct hook into (tabs)/home/index.jsx, and would like to see why the home screen does not re-render. The code is shown below:

1
2
3
4
5
useEffect(() => {
console.log(`language: ${language}`);
console.log(`i18n.language: ${i18n.language}`);
i18next.changeLanguage(i18n.language);
}, []);

Then I changed the language and i18n.language from the set-language again, I found out that as soon as I changed the variables, the logs were shown on the terminal (means that the useEffect hook in the home screen was triggered). When I switched back to the home screen, the terminal did not print out the logs again (means that useEffect hook was not trigered).

Hence, I reached to the conclusion that under the tabs navigation of Expo Router, the tabs are somehow binded together, i.e. change states in one tab, will cause the useEffect hooks in the other tabs triggered. In this case, the other tabs consider themselves are re-rendered already. For example, in my case, I changed the language state under my-account/set-language, the useEffect hook in the home screen is triggerd, since I am still in the set-language screen, the components in the home screen will not be re-endered. When I switch back to the home screen, as there is no state change, the useEffect hook will not be triggered, and the language in the home screen stay with the old one.

The Solution

After identifying the root cause, the thought for the solution is obvious: find a method to ‘force’ the (tabs)/home screen to be re-rendered when change the language state in the (tabs)/my-account/set-language screen.

Enlightened by the information provided from Device context doesn’t work with Expo Router, I use the i18n.language as the key value for the Stack component under app/_layout.js (which is the parent componet for (tabs) ). Therefore, everytime the i18n.language is changed, the key value of the Stack component of app/_layout.js will be changed, thus cause all tabs under the (tabs) directory be re-rendered. The source code is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...

const Layout = () => {
const { i18n } = useTranslation()

return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Stack key={JSON.stringify(i18n?.language)}>
<Stack.Screen name='(tabs)' options={{ headerShown: false }} />
</Stack>
</PersistGate>
</Provider>
)
}
...

Compile and run the app again. Bingo! The feature works as expcted :)

Comment and share

In the last few months, I am working on a Defi app which is based on the Expo platform. One of the ‘must have’ thing is that the app need to store insensitive user data locally, such as app settings. After comparing with several potential solutions, like react-native-mmkv (Not compatible with Expo), expo-sqlite (Not compatiblw with the Expo for web), redux-persist (build on top of Async Storage, compatible with Android, iOS and web, easy to use), I decided to adopt Redux.

I searched a bit regarding integrating Redux into Expo, however, in my Expo project, I used tabs navigation structure, so there is no app.js as the main entry (The file structure is listed blow). And all the solutions that I found only works on the Expo project that has an App component (normally app.js) as an entry point. After a few hours of trouble shooting, I finally found the solution myself and decided to share the method in this article, hopefully it will help someone in the future.

1
2
3
4
5
6
7
8
9
10
11
-- root
-- app
-- (tabs)
-- home
...
-- index.js // entry
-- _layout.js
...
...
-- index.js
...

Step by Step Guide

Although the way to the destination is rugged, the solution is quite simple. To embed Redux into an Expo project with the tabs navigation structure, we only need to add the redux provider into two files: app/index.js and app/_lauout.js.

app/index.js

As shown below, we wrap the <Redirect href="/home" /> with the Redux provider and the PersistGate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useRootNavigationState, Redirect } from 'expo-router';

import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/es/integration/react';
// store and persistor are self-defined common redux component, nothing special
import { store, persistor } from '../services/redux/store';

const Home = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Redirect href="/home" />
</PersistGate>
</Provider>
);
};

export default Home;

app/_layout.js

This is the tricky part, I could not image that to make Redux work, I need to wrap the Stack with the Redux provider and the PersistGate until I found out that this is a key step.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Stack } from 'expo-router';

import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/es/integration/react';
import { store, persistor } from '../services/redux/store';

const Layout = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
</PersistGate>
</Provider>
);
};

export default Layout;

After adding the two, I am able to use the Redux in the files under /app. Happy coding: )

Comment and share

  • page 1 of 1
Author's picture

Jingjie Jiang


Find a place I love the most