Skip to content

Commit 823f19d

Browse files
lucjMano Marks
authored and
Mano Marks
committed
First version of 12factor app methodology with Node.js and Docker (docker#27)
1 parent c5b0b71 commit 823f19d

14 files changed

+588
-0
lines changed

12factor/00_application.md

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Build the application
2+
3+
To illustrate the 12 factors, we start by creating a simple Node.js application as a HTTP Rest API exposing CRUD verbs on a *message* model.
4+
5+
There is a couple of prerequisite to build this application
6+
* [Node.js 4.4.5 (LTS)](https://nodejs.org/en/)
7+
* [mongo 3.2](https://docs.mongodb.org/manual/installation/)
8+
9+
## Routes exposed
10+
11+
HTTP verb | URI | Action
12+
----------| --- | ------
13+
GET | /message | list all messages
14+
GET | /message/ID | get message with ID
15+
POST | /message | create a new message
16+
PUT | /message/ID | modify message with ID
17+
DELETE | /message/ID | delete message with ID
18+
19+
## Setup
20+
21+
* Install Sails.js (it's to Node.js what RoR is to Ruby): `sudo npm install sails -g`
22+
* Create the application: `sails new messageApp && cd messageApp`
23+
24+
## First tests
25+
26+
Create new messages
27+
28+
```
29+
curl -XPOST http://localhost:1337/message?text=hello
30+
curl -XPOST http://localhost:1337/message?text=hola
31+
```
32+
33+
Get list of messages
34+
35+
```
36+
curl http://localhost:1337/message
37+
38+
[
39+
{
40+
"text": "hello",
41+
"createdAt": "2015-11-08T13:15:15.363Z",
42+
"updatedAt": "2015-11-08T13:15:15.363Z",
43+
"id": "5638b363c5cd0825511690bd"
44+
},
45+
{
46+
"text": "hola",
47+
"createdAt": "2015-11-08T13:15:45.774Z",
48+
"updatedAt": "2015-11-08T13:15:45.774Z",
49+
"id": "5638b381c5cd0825511690be"
50+
}
51+
]
52+
```
53+
54+
Modify a message
55+
56+
```
57+
curl -XPUT http://localhost:1337/message/5638b363c5cd0825511690bd?text=hey
58+
```
59+
60+
Delete a message
61+
62+
```
63+
curl -XDELETE http://localhost:1337/message/5638b381c5cd0825511690be
64+
```
65+
66+
Get updates list of messages
67+
68+
```
69+
curl http://localhost:1337/message
70+
71+
[
72+
{
73+
"text": "hey",
74+
"createdAt": "2015-11-08T13:15:15.363Z",
75+
"updatedAt": "2015-11-08T13:19:40.179Z",
76+
"id": "5638b363c5cd0825511690bd"
77+
}
78+
]
79+
```
80+
81+
[Next](01_codebase.md)

12factor/01_codebase.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# 1 - Codebase
2+
3+
**one application <=> one codebase**
4+
5+
If there are several codebase, it's not an application, it's a distributed system containing multiple applications.
6+
7+
One codebase used for several deployments of the application
8+
* development
9+
* staging
10+
* production
11+
12+
## What does that mean for our application ?
13+
14+
We will use Git versioning system (could have chosen subversion, ...) to handle our source code.
15+
16+
* Create a repo on [Github](https://github.com)
17+
* Put the code under git
18+
19+
```
20+
$ echo "# messageApp" >> README.md
21+
$ git init
22+
$ git add .
23+
$ git commit -m 'First commit'
24+
$ git remote add origin git@github.com:GITUSER/messageApp.git
25+
$ git push origin master
26+
```
27+
28+
The update we've done is not linked with Docker, but we'll see in a next chapter that this will greatly help the integration of several components of the Docker ecosystem.
29+
30+
[Previous](00_application.md) - [Next](02_dependencies.md)

12factor/02_dependencies.md

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# 2 - Dependencies
2+
3+
Application's dependencies must be declared and isolated
4+
5+
## What does that mean for our application ?
6+
7+
Declaration are done in package.json file.
8+
9+
Let's add sails-mongo (mongodb driver) as we'll need it very quicky
10+
11+
`npm install sails-mongo --save`
12+
13+
The package.json file should look like the following:
14+
15+
```
16+
{
17+
"name": "messageApp",
18+
"private": true,
19+
"version": "0.0.0",
20+
"description": "a Sails application",
21+
"keywords": [],
22+
"dependencies": {
23+
"ejs": "2.3.4",
24+
"grunt": "0.4.5",
25+
"grunt-contrib-clean": "0.6.0",
26+
"grunt-contrib-coffee": "0.13.0",
27+
"grunt-contrib-concat": "0.5.1",
28+
"grunt-contrib-copy": "0.5.0",
29+
"grunt-contrib-cssmin": "0.9.0",
30+
"grunt-contrib-jst": "0.6.0",
31+
"grunt-contrib-less": "1.1.0",
32+
"grunt-contrib-uglify": "0.7.0",
33+
"grunt-contrib-watch": "0.5.3",
34+
"grunt-sails-linker": "~0.10.1",
35+
"grunt-sync": "0.2.4",
36+
"include-all": "~0.1.6",
37+
"rc": "1.0.1",
38+
"sails": "~0.12.3",
39+
"sails-disk": "~0.10.9",
40+
"sails-mongo": "^0.12.0" // Newly added dependency
41+
},
42+
"scripts": {
43+
"debug": "node debug app.js",
44+
"start": "node app.js"
45+
},
46+
"main": "app.js",
47+
"repository": {
48+
"type": "git",
49+
"url": "git://github.com/GITUSER/messageApp.git"
50+
},
51+
"author": "AUTHOR",
52+
"license": ""
53+
}
54+
```
55+
56+
Dependencies are isolated within _node-modules_ folder where all the [npm](https://npmjs.org) libraries are compiled and installed.
57+
58+
```
59+
$ ls node_modules/
60+
ejs grunt-contrib-coffee grunt-contrib-cssmin grunt-contrib-uglify grunt-sync sails
61+
grunt grunt-contrib-concat grunt-contrib-jst grunt-contrib-watch include-all sails-disk
62+
grunt-contrib-clean grunt-contrib-copy grunt-contrib-less grunt-sails-linker rc sails-mongo
63+
```
64+
65+
[Previous](01_codebase.md) - [Next](03_configuration.md)

12factor/03_configuration.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# 3 - Configuration
2+
3+
Configuration (credentials, database connection string, ...) should be stored in the environment.
4+
5+
## What does that mean for our application ?
6+
7+
In _config/connections.js_, we define the _mongo_ connection and use MONGO_URL environment variable to pass the mongo connection string.
8+
9+
```
10+
module.exports.connections = {
11+
mongo: {
12+
adapter: 'sails-mongo',
13+
url: process.env.MONGO_URL'
14+
}
15+
};
16+
```
17+
18+
In _config/model.js_, we make sure the _mongo_ connection defined above is the one used.
19+
20+
```
21+
module.exports.models = {
22+
connection: mongo,
23+
migrate: 'safe'
24+
};
25+
```
26+
27+
Those changes enable to provide a different _MONGO_URL_ very easily as it's defined in the environment.
28+
29+
[Previous](02_dependencies.md) - [Next ](04_external_services.md)

12factor/04_external_services.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# 4 - External services
2+
3+
Handle external services as external resources of the application.
4+
5+
Examples:
6+
* database
7+
* log services
8+
* ...
9+
10+
This ensure the application is loosely coupled with the services so it can easily switch provider or instance if needed
11+
12+
## What does that mean for our application ?
13+
14+
At this point, the only external service the application is using is MongoDB database. The loosely coupling is already done by the MONGO_URL used to pass the connection string.
15+
16+
If something wrong happens with our instance of MongoDB (assuming a single instance is used, which is generally a bad idea...), we can easily switch to a new instance, providing a new MONGO_URL environment variable and restart the application.
17+
18+
[Previous](03_configuration.md) - [Next](05_build_release_run.md)

12factor/05_build_release_run.md

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# 5 - Build / Release / Run
2+
3+
Build / Release and Run phases must be kept separated
4+
5+
![Build/Release/Run](https://dl.dropboxusercontent.com/u/2330187/docker/labs/12factor/build_release_run.png)
6+
7+
A release is deployed on the execution environment and must be immutable.
8+
9+
## What does that mean for our application ?
10+
11+
We'll use Docker in the whole development pipeline. We will start by adding a Dockerfile that will help define the build phase (during which the dependencies are compiled in _node-modules_ folder)
12+
13+
```
14+
FROM node:4.4.5
15+
ENV LAST_UPDATED 20160617T185400
16+
17+
# Copy source code
18+
COPY . /app
19+
20+
# Change working directory
21+
WORKDIR /app
22+
23+
# Install dependencies
24+
RUN npm install
25+
26+
# Expose API port to the outside
27+
ENV PORT 80
28+
EXPOSE 80
29+
30+
# Launch application
31+
CMD ["npm","start"]
32+
```
33+
34+
Let's build our application `$ docker build -t message-app:v0.1 .`
35+
36+
And verify the resulting image is in the list of available images
37+
38+
```
39+
$ docker images
40+
REPOSITORY TAG IMAGE ID CREATED SIZE
41+
message-app v0.1 f35464cf4b0b 2 seconds ago 769 MB
42+
```
43+
44+
Now the image (build) is available, execution environment must be injected to create a release.
45+
46+
There are several options to inject the configuration in the build, among them
47+
* create a new image based on the build
48+
* define a Compose file
49+
50+
We'll go for the second option and define a docker-compose file where the MONGO_URL will be set with the value of the execution environment
51+
52+
```
53+
version: '2'
54+
services:
55+
mongo:
56+
image: mongo:3.2
57+
volumes:
58+
- mongo-data:/data/db
59+
expose:
60+
- "27017"
61+
app:
62+
image: message-app:v0.1
63+
ports:
64+
- "8000:80"
65+
links:
66+
- mongo
67+
depends_on:
68+
- mongo
69+
environment:
70+
- MONGO_URL=mongodb://mongo/messageApp
71+
volumes:
72+
mongo-data:
73+
```
74+
75+
This file defines a release as it considers a given build and inject the execution environment.
76+
77+
The run phase can be done manually with Compose CLI or through an orchestrator (Docker Cloud).
78+
79+
Compose CLI enables to run the global application as simple as `docker-compose up -d`
80+
81+
[Previous](04_external_services.md) - [Next](06_processes.md)

12factor/06_processes.md

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# 6 - Processes
2+
3+
An application is made up of several processes.
4+
5+
Each process must be stateless and must not have local storage (sessions, ...).
6+
7+
This is required
8+
* for scalability
9+
* fault tolerance (crashes, ...)
10+
11+
The data that need to be persisted, must be saved in a stateful resources (Database, shared filesystem, ...)
12+
13+
Eg: sessions can easily be saved in a Redis kv store
14+
15+
Note: Sticky session violate 12 factor.
16+
17+
## What does that mean for our application ?
18+
19+
In _config/sessions.js_, we need to modify the adapter to store session in a distributed Redis kv store (MongoDB is another possible option).
20+
21+
```
22+
module.exports.session = {
23+
...
24+
adapter: 'redis',
25+
host: process.env.REDIS_HOST || 'localhost',
26+
...
27+
};
28+
```
29+
30+
Once done, the app needs to be rebuilt `docker build -t message-app:v0.2 .`
31+
32+
**REDIS_HOST** needs to be added to the docker-compose file as the new release will run against this kv store.
33+
34+
```
35+
version: '2'
36+
services:
37+
mongo:
38+
image: mongo:3.2
39+
volumes:
40+
- mongo-data:/data/db
41+
expose:
42+
- "27017"
43+
kv:
44+
image: redis:alpine
45+
volumes:
46+
- redis-data:/data
47+
expose:
48+
- "6379"
49+
app:
50+
image: message-app:v0.2 # New version taking into account REDIS_URL
51+
ports:
52+
- "8000:80"
53+
links:
54+
- mongo
55+
depends_on:
56+
- mongo
57+
environment:
58+
- MONGO_URL=mongodb://mongo/messageApp
59+
- REDIS_URL=redis
60+
volumes:
61+
mongo-data:
62+
redis-data:
63+
```
64+
65+
[Previous](05_build_release_run.md) - [Next](07_port_binding.md)

0 commit comments

Comments
 (0)