Docker compose: MEAN stack
We'll be building two containers, Angular and the Express/Node API. The third container will be from a MongoDB image that we'll pull from the Docker Hub.
We need to know how to build a simple Angular 10 app and an Express App. We'll be using the Angular CLI to build a simple app. So install the Angular CLI:
$ npm install -g @angular/cli@10 npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142 npm WARN deprecated har-validator@5.1.5: this library is no longer supported /usr/local/bin/ng -> /usr/local/lib/node_modules/@angular/cli/bin/ng > @angular/cli@10.1.3 postinstall /usr/local/lib/node_modules/@angular/cli > node ./bin/postinstall/script.js + @angular/cli@10.1.3 updated 7 packages in 10.875s
Create a directory for our apps:
$ mkdir mean-10
Let's create an Angular 10 app and make sure it runs in a docker container. Create an angular app called "angular-app" with the command ng new
:
$ ng new angular-app ? Would you like to add Angular routing? Yes ? Which stylesheet format would you like to use? CSS CREATE angular-app/README.md (1028 bytes) CREATE angular-app/.editorconfig (274 bytes) CREATE angular-app/.gitignore (631 bytes) CREATE angular-app/angular.json (3606 bytes) CREATE angular-app/package.json (1254 bytes) CREATE angular-app/tsconfig.json (458 bytes) CREATE angular-app/tslint.json (3185 bytes) CREATE angular-app/.browserslistrc (853 bytes) CREATE angular-app/karma.conf.js (1023 bytes) CREATE angular-app/tsconfig.app.json (287 bytes) CREATE angular-app/tsconfig.spec.json (333 bytes) CREATE angular-app/src/favicon.ico (948 bytes) CREATE angular-app/src/index.html (296 bytes) CREATE angular-app/src/main.ts (372 bytes) CREATE angular-app/src/polyfills.ts (2835 bytes) CREATE angular-app/src/styles.css (80 bytes) CREATE angular-app/src/test.ts (753 bytes) CREATE angular-app/src/assets/.gitkeep (0 bytes) CREATE angular-app/src/environments/environment.prod.ts (51 bytes) CREATE angular-app/src/environments/environment.ts (662 bytes) CREATE angular-app/src/app/app-routing.module.ts (245 bytes) CREATE angular-app/src/app/app.module.ts (393 bytes) CREATE angular-app/src/app/app.component.css (0 bytes) CREATE angular-app/src/app/app.component.html (25757 bytes) CREATE angular-app/src/app/app.component.spec.ts (1072 bytes) CREATE angular-app/src/app/app.component.ts (215 bytes) CREATE angular-app/e2e/protractor.conf.js (869 bytes) CREATE angular-app/e2e/tsconfig.json (294 bytes) CREATE angular-app/e2e/src/app.e2e-spec.ts (644 bytes) CREATE angular-app/e2e/src/app.po.ts (301 bytes) ✔ Packages installed successfully. Successfully initialized git.
This scaffolds an angular app, and npm installs the app's dependencies. Our directory structure should be like this:
$ tree mean-10 -L 2 mean-10 └── angular-app ├── README.md ├── angular.json ├── e2e ├── karma.conf.js ├── node_modules ├── package-lock.json ├── package.json ├── src ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json 4 directories, 9 files
(note) To avoid some issues with dependencies, please replace the package.json with this:
{ "name": "angular-app", "version": "0.0.0", "license": "MIT", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build --prod", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" }, "private": true, "dependencies": { "@angular/common": "^5.2.0", "@angular/compiler": "^5.2.0", "@angular/core": "^5.2.0", "@angular/forms": "^5.2.0", "@angular/http": "^5.2.0", "@angular/platform-browser": "^5.2.0", "@angular/platform-browser-dynamic": "^5.2.0", "@angular/router": "^5.2.0", "bootstrap": "^4.2.1", "core-js": "^2.4.1", "font-awesome": "^4.7.0", "rxjs": "^5.5.6", "zone.js": "^0.8.19" }, "devDependencies": { "@angular/cli": "~1.7.4", "@angular/compiler-cli": "^5.2.0", "@angular/language-service": "^5.2.0", "@types/jasmine": "~2.8.3", "@types/jasminewd2": "~2.0.2", "@types/node": "~6.0.60", "codelyzer": "^4.0.1", "jasmine-core": "~2.8.0", "jasmine-spec-reporter": "~4.2.1", "karma": "~2.0.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "^1.2.1", "karma-jasmine": "~1.1.0", "karma-jasmine-html-reporter": "^0.2.2", "protractor": "~5.1.2", "ts-node": "~4.1.0", "tslint": "~5.9.1", "typescript": "~2.5.3" } }
Source code: MEAN-Docker.
Running ng serve
inside the angular-app directory should start the angular app at http://localhost:4200:
~/mean-10/angular-app $ ng serve Compiling @angular/core : es2015 as esm2015 Compiling @angular/common : es2015 as esm2015 Compiling @angular/platform-browser : es2015 as esm2015 Compiling @angular/router : es2015 as esm2015 Compiling @angular/platform-browser-dynamic : es2015 as esm2015 chunk {main} main.js, main.js.map (main) 59.7 kB [initial] [rendered] chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 141 kB [initial] [rendered] chunk {runtime} runtime.js, runtime.js.map (runtime) 6.15 kB [entry] [rendered] chunk {styles} styles.js, styles.js.map (styles) 12.5 kB [initial] [rendered] chunk {vendor} vendor.js, vendor.js.map (vendor) 2.62 MB [initial] [rendered] Date: 2020-09-24T19:26:28.099Z - Hash: f1b4820ac18773ffde79 - Time: 10957ms ** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ ** : Compiled successfully. Date: 2020-09-24T19:26:29.172Z - Hash: f1b4820ac18773ffde79 5 unchanged chunks Time: 699ms : Compiled successfully.
mean-10/angular-app/Dockerfile:
# Stage 1 # Create image based on the official Node 12 image from dockerhub FROM node:12.18.4-alpine as node # Create a directory where our app will be placed RUN mkdir -p /usr/src/app # Change directory so that our commands run inside this new directory WORKDIR /usr/src/app # Copy dependency definitions COPY package*.json ./ # Install dependecies RUN npm install # Get all the code needed to run the app COPY . . # Run the angular in production mode RUN npm run build # Stage 2 FROM nginx:1.19.2-alpine # Copy dist content to html nginx folder, config nginx to point in index.html COPY --from=node /usr/src/app/dist /usr/share/nginx/html COPY ./nginx.conf /etc/nginx/conf.d/default.conf
mean-10/angular-app/nginx.conf:
server { listen 80; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html =404; } }
Now that we've containerized the angular app, let's containerize our express app. Create a directory in the "mean-10' directory called "express-server":
$ mkdir express-server
mean-10/express-server/package.json:
{ "name": "express-server", "version": "0.0.0", "private": true, "scripts": { "start": "node server.js" }, "dependencies": { "body-parser": "^1.18.2", "express": "^4.17.1" } }
express-server/routes/api.js:
const express = require('express'); const router = express.Router(); /* GET api listing. */ router.get('/', (req, res) => { res.send('api works'); }); module.exports = router;
express-server/server.js:
// Get dependencies const express = require('express'); const path = require('path'); const http = require('http'); const bodyParser = require('body-parser'); // Get our API routes const api = require('./routes/api'); const app = express(); // Parsers for POST data app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); // Set our api routes app.use('/', api); /** * Get port from environment and store in Express. */ const port = process.env.PORT || '3000'; app.set('port', port); /** * Create HTTP server. */ const server = http.createServer(app); /** * Listen on provided port, on all network interfaces. */ server.listen(port, () => console.log(`API running on localhost:${port}`));
Install the dependencies for the express app and start the app:
$ npm install $ npm start
We should see the app with localhost:3000 in our browser:
To run this app inside a docker container, we need to create a express-server/Dockerfile for it:
# Create image based on the official Node 6 image from the dockerhub FROM node:12 # Create a directory where our app will be placed RUN mkdir -p /usr/src/app # Change directory so that our commands run inside this new directory WORKDIR /usr/src/app # Copy dependency definitions COPY package.json /usr/src/app # Install dependecies RUN npm install # Get all the code needed to run the app COPY . /usr/src/app # Expose the port the app runs in EXPOSE 3000 # Serve the app CMD ["npm", "start"]
Let's build the image and run a container based on the image:
$ docker build -t express-server:dev . $ docker run -d --name express-server -p 3000:3000 express-server:dev
With localhost:3000 in our browser, we should see the api:
Once we are done, we can stop the container:
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES df82b4496eaf express-server:dev "docker-entrypoint.s…" 5 minutes ago Up 5 minutes 0.0.0.0:3000->3000/tcp express-server $ docker stop express-server express-server
We run a container based on the image pulled from DockerHub:
$ docker run -d --name mongodb -p 27017:27017 mongo $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4216c5110db3 mongo "docker-entrypoint.s…" 4 seconds ago Up 3 seconds 0.0.0.0:27017->27017/tcp mongodb
By going into http://localhost:27017 of the browser, we can check if mongodb is running and we should see this message: "It looks like you are trying to access MongoDB over HTTP on the native driver port".
Let's run mongo
in the terminal and it should give us a mongo shell:
$ docker exec -it mongodb mongo MongoDB shell version v4.4.1 connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb Implicit session: session { "id" : UUID("35adefdd-cef5-4131-9378-c4a4ec9c87ed") } MongoDB server version: 4.4.1 Welcome to the MongoDB shell. ...
mean-10/docker-compose.yml:
version: '2' # specify docker-compose version # Define the services/containers to be run services: angular: # name of the first service hostname: localhost build: angular-app # specify the directory of the Dockerfile ports: - "8181:80" # specify port forewarding express: #name of the second service build: express-server # specify the directory of the Dockerfile ports: - "3000:3000" #specify ports forewarding database: # name of the third service image: mongo # specify image to build container from ports: - "27017:27017" # specify port forewarding
mean-10 ├── angular-app │ ├── Dockerfile │ ├── README.md │ ├── angular.json │ ├── e2e │ ├── karma.conf.js │ ├── node_modules │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── tslint.json ├── docker-compose.yml └── express-server ├── Dockerfile ├── node_modules ├── package-lock.json ├── package.json ├── routes └── server.js
Let's run containers based on the three images specified in the file:
$ docker-compose up ... express_1 | API running on localhost:3000
If we change a service's Dockerfile or the contents of its build directory, we need to run docker-compose build
to rebuild it.
Now, we need to connect the containers. First, we will create a simple CRUD feature in our api using mongoose. So, let's add mongoose to our express server package.json, mean-10/express-server/package.json:
{ "name": "express-server", "version": "0.0.0", "private": true, "scripts": { "start": "node server.js" }, "dependencies": { "body-parser": "^1.18.2", "express": "^4.17.1", "mongoose": "^5.0.15" } }
Now we need to update our api (mean-10/express-server/routes/api.js) to use mongo:
// Import dependencies const mongoose = require('mongoose'); const express = require('express'); const router = express.Router(); // MongoDB URL from the docker-compose file const dbHost = 'mongodb://database/mean-docker'; // Connect to mongodb mongoose.connect(dbHost); // create mongoose schema const userSchema = new mongoose.Schema({ name: String, age: Number }); // create mongoose model const User = mongoose.model('User', userSchema); /* GET api listing. */ router.get('/', (req, res) => { res.send('api works'); }); /* GET all users. */ router.get('/users', (req, res) => { User.find({}, (err, users) => { if (err) res.status(500).send(error) res.status(200).json(users); }); }); /* GET one users. */ router.get('/users/:id', (req, res) => { User.findById(req.param.id, (err, users) => { if (err) res.status(500).send(error) res.status(200).json(users); }); }); /* Create a user. */ router.post('/users', (req, res) => { let user = new User({ name: req.body.name, age: req.body.age }); user.save(error => { if (error) res.status(500).send(error); res.status(201).json({ message: 'User created successfully' }); }); }); module.exports = router;
Note that we've added rest routes GET /users, GET /users/:id and POST /user.
Update the mean-10/docker-compose file telling the express service to link to the database service:
version: '2' # Define the services/containers to be run services: angular: hostname: localhost build: angular-app ports: - "8181:80" express: build: express-server ports: - "3000:3000" links: - database database: image: mongo ports: - "27017:27017"
The links property of the docker-compose file creates a connection to the other service with the name of the service as the host name. In other words, to connect to mongodb from the express service, we should use database:27017. That's why we made the dbHost equal to mongodb://database/mean-docker in express-server/routes/api.js.
The last step of connecting containers is to connect the Angular app to the express server. To do this, we'll need to make some modifications to our angular app to consume the express api, mean-10/angular-app/src/app/app.component.ts from:
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'angular-app'; }
to:
import { Component, OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { map } from 'rxjs/operators'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { title = 'app works!'; // Link to our api, pointing to localhost API = 'http://localhost:3000'; // Declare empty list of people people: any[] = []; constructor(private http: HttpClient) {} // Angular 2 Life Cycle event when component has been initialized ngOnInit() { this.getAllPeople(); } // Add one person to the API addPerson(name, age) { this.http.post(`${this.API}/users`, {name, age}) .subscribe((data: any) => { this.getAllPeople(); }, (error: any) => {console.log(error);}); } // Get all users from the API getAllPeople() { this.http.get(`${this.API}/users`) .subscribe((people : any )=> { console.log(people) this.people = people }, (error: any) => {console.log(error);}); } }
In the code, to call events when the component is initialized, we've imported the OnInit interface and then added a two methods AddPerson and getAllPeople that call the API.
Note also that our API is pointing to localhost because the browser is the one that makes request to the exposed express API. So, we don't need to link Angular and Express in the docker-compose.yml file.
Since we've made changes to our code, we need to do a build for our Docker Compose
$ docker-compose up --build
The --build flag tells docker compose that we've made changes and it needs to do a clean build of our images.
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c7f85ac61ee8 mean-10_express "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:3000->3000/tcp mean-10_express_1 d3c20d9d5ec1 mean-10_angular "/docker-entrypoint.…" 33 minutes ago Up About a minute 0.0.0.0:8181->80/tcp mean-10_angular_1 61bb6eb3aa6c mongo "docker-entrypoint.s…" 3 hours ago Up About a minute 0.0.0.0:27017->27017/tcp mean-10_database_1
AngularJS
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization