mirror of
https://github.com/bitnami/containers.git
synced 2026-03-16 14:57:44 +08:00
example: adds production example
This commit is contained in:
2
bitnami/node/example/.dockerignore
Normal file
2
bitnami/node/example/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
kubernetes.yml
|
||||
README.md
|
||||
30
bitnami/node/example/.gitignore
vendored
Normal file
30
bitnami/node/example/.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# https://docs.npmjs.com/cli/shrinkwrap#caveats
|
||||
node_modules
|
||||
|
||||
# Debug log from npm
|
||||
npm-debug.log
|
||||
11
bitnami/node/example/Dockerfile
Normal file
11
bitnami/node/example/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM bitnami/node:6-prod
|
||||
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
COPY . /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install
|
||||
|
||||
CMD ["npm", "start"]
|
||||
132
bitnami/node/example/README.md
Normal file
132
bitnami/node/example/README.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Example Application
|
||||
|
||||
## TL;DR
|
||||
|
||||
```bash
|
||||
$ kubectl create -f https://raw.githubusercontent.com/bitnami/bitnami-docker-node/master/example/kubernetes.yml
|
||||
```
|
||||
|
||||
## Introduction
|
||||
|
||||
This example demostrates the use of the `bitnami/node` image to create a production build of your node application.
|
||||
|
||||
For demonstration purposes we'll bootstrap a [ExpressJS](https://expressjs.com) application, build a image with the tag `bitnami/node-example` and deploy it on a [Kubernetes](https://kubernetes.io) cluster.
|
||||
|
||||
## Generate the application
|
||||
|
||||
The example application is an [ExpressJS](https://expressjs.com) application bootstrapped using [express-generator](https://www.npmjs.com/package/express-generator).
|
||||
|
||||
```bash
|
||||
$ express --git --css less example/
|
||||
```
|
||||
|
||||
## Build and Test
|
||||
|
||||
To build a production Docker image of our application we'll use the `bitnami/node:6-prod` image, which is a production build of the Bitnami Node Image optimized for size.
|
||||
|
||||
```dockerfile
|
||||
FROM bitnami/node:6-prod
|
||||
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
COPY . /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install
|
||||
|
||||
CMD ["npm", "start"]
|
||||
```
|
||||
|
||||
We use the above `Dockerfile` to `COPY` the example application at the `/app` path of the container and install the npm module dependencies with the command `npm install`. Finally the Express application is start with `npm start`.
|
||||
|
||||
To build the Docker image, execute the command:
|
||||
|
||||
```bash
|
||||
$ docker build -t bitnami/node-example:0.0.1 example/
|
||||
```
|
||||
|
||||
Since the `bitnami/node:6-prod` image is optimized for production deployments it does not include any packages that would bloat the image.
|
||||
|
||||
```console
|
||||
$ docker image ls
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
bitnami/node-example 0.0.1 0d43bbca1cd2 22 seconds ago 193MB
|
||||
```
|
||||
|
||||
You can now launch and test the image locally.
|
||||
|
||||
```console
|
||||
$ docker run -it --rm -p 3000:3000 bitnami/node-example:0.0.1
|
||||
|
||||
> example@0.0.1 start /app
|
||||
> node ./bin/www
|
||||
```
|
||||
|
||||
Finally, push the image to the Docker registry
|
||||
|
||||
```bash
|
||||
$ docker push bitnami/node-example:0.0.1
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
The `kubernetes.yml` file from the `example/` folder can be used to deploy our `bitnami/node-example:0.0.1` image to a Kubernetes cluster.
|
||||
|
||||
Simply download the Kubernetes manifest and create the Kubernetes resources described in the manifest using the command:
|
||||
|
||||
```console
|
||||
$ kubectl create -f kubernetes.yml
|
||||
ingress "example-ingress" created
|
||||
service "example-svc" created
|
||||
configmap "example-configmap" created
|
||||
persistentvolumeclaim "example-data-pvc" created
|
||||
deployment "example-deployment" created
|
||||
```
|
||||
|
||||
From the output of the above command you will notice that we create the following resources:
|
||||
|
||||
- [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/)
|
||||
- [Service](https://kubernetes.io/docs/concepts/services-networking/service/)
|
||||
- [Volume](https://kubernetes.io/docs/concepts/storage/volumes/)
|
||||
+ [ConfigMap](https://kubernetes.io/docs/concepts/storage/volumes/#projected)
|
||||
+ [PersistentVolumeClaim](https://kubernetes.io/docs/concepts/storage/volumes/#persistentvolumeclaim)
|
||||
- [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> Our example application is stateless and does not store any data or does not require any user configurations. As such we do not need to create the `ConfigMap` or `PersistentVolumeClaim` resources. Our `kubernetes.yml` creates these resources strictly to demostrate how they are defined in the manifest.
|
||||
|
||||
## Accessing the application
|
||||
|
||||
Typically in production you would access the application via a Ingress controller. Our `kubernetes.yml` already defines a `Ingress` resource. Please refer to the [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) documentation to learn how to deploy an ingress controller in your cluster.
|
||||
|
||||
> **Hint**
|
||||
>
|
||||
> https://kubeapps.com/charts/stable/nginx-ingress
|
||||
|
||||
The following are alternate ways of accessing the application, typically used during application development and testing.
|
||||
|
||||
Since the service `example-svc` is defined to be of type `NodePort`, we can set up port forwarding to access our web application like so:
|
||||
|
||||
```bash
|
||||
$ kubectl port-forward $(kubectl get pods -l app=example -o jsonpath="{ .items[0].metadata.name }") 3000:3000
|
||||
```
|
||||
|
||||
The command forwards the local port `3000` to port `3000` of the Pod container. You can access the application by visiting the http://localhost:3000.
|
||||
|
||||
> **Note:**
|
||||
>
|
||||
> If your using minikube, you can access the application by simply executing the following command:
|
||||
>
|
||||
> ```bash
|
||||
> $ minikube service example-svc
|
||||
> ```
|
||||
|
||||
## Health Checks
|
||||
|
||||
The `kubernetes.yml` manifest defines default probes to check the health of the application. For our application we are simply probing if the application is responsive to queries on the root resource.
|
||||
|
||||
You application can define a route, such as the commonly used `/healthz`, that reports the application status and use that route in the health probes.
|
||||
|
||||
|
||||
48
bitnami/node/example/app.js
Normal file
48
bitnami/node/example/app.js
Normal file
@@ -0,0 +1,48 @@
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var favicon = require('serve-favicon');
|
||||
var logger = require('morgan');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var bodyParser = require('body-parser');
|
||||
var lessMiddleware = require('less-middleware');
|
||||
|
||||
var index = require('./routes/index');
|
||||
var users = require('./routes/users');
|
||||
|
||||
var app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
|
||||
// uncomment after placing your favicon in /public
|
||||
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
|
||||
app.use(logger('dev'));
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(lessMiddleware(path.join(__dirname, 'public')));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.use('/', index);
|
||||
app.use('/users', users);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function(req, res, next) {
|
||||
var err = new Error('Not Found');
|
||||
err.status = 404;
|
||||
next(err);
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use(function(err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
90
bitnami/node/example/bin/www
Executable file
90
bitnami/node/example/bin/www
Executable file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var app = require('../app');
|
||||
var debug = require('debug')('example:server');
|
||||
var http = require('http');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
var server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
}
|
||||
112
bitnami/node/example/kubernetes.yml
Normal file
112
bitnami/node/example/kubernetes.yml
Normal file
@@ -0,0 +1,112 @@
|
||||
apiVersion: v1
|
||||
items:
|
||||
- apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: example-ingress
|
||||
labels:
|
||||
app: example
|
||||
annotations:
|
||||
spec:
|
||||
rules:
|
||||
- host: app.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
backend:
|
||||
serviceName: example-svc
|
||||
servicePort: 80
|
||||
tls:
|
||||
- apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: example-svc
|
||||
labels:
|
||||
app: example
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: http
|
||||
selector:
|
||||
app: example
|
||||
type: NodePort
|
||||
status:
|
||||
loadBalancer: {}
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: example-configmap
|
||||
labels:
|
||||
app: example
|
||||
data:
|
||||
config.js: |-
|
||||
- apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: example-data-pvc
|
||||
annotations: {}
|
||||
labels:
|
||||
app: example
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 100Mi
|
||||
status: {}
|
||||
- apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: example-deployment
|
||||
labels:
|
||||
app: example
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: example
|
||||
spec:
|
||||
containers:
|
||||
- name: example
|
||||
image: bitnami/node-example:0.0.1
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: http
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
httpHeaders:
|
||||
- name: Host
|
||||
value: app.example.com
|
||||
initialDelaySeconds: 15
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
httpHeaders:
|
||||
- name: Host
|
||||
value: app.example.com
|
||||
initialDelaySeconds: 5
|
||||
resources: {}
|
||||
volumeMounts:
|
||||
- mountPath: /app/config.js
|
||||
name: example-config
|
||||
subPath: config.js
|
||||
- mountPath: /app/data
|
||||
name: example-data
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
- name: example-config
|
||||
configMap:
|
||||
name: example-configmap
|
||||
- name: example-data
|
||||
persistentVolumeClaim:
|
||||
claimName: example-data-pvc
|
||||
status: {}
|
||||
kind: List
|
||||
metadata: {}
|
||||
18
bitnami/node/example/package.json
Normal file
18
bitnami/node/example/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "example",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "~1.17.1",
|
||||
"cookie-parser": "~1.4.3",
|
||||
"debug": "~2.6.3",
|
||||
"express": "~4.15.2",
|
||||
"jade": "~1.11.0",
|
||||
"less-middleware": "~2.2.0",
|
||||
"morgan": "~1.8.1",
|
||||
"serve-favicon": "~2.4.2"
|
||||
}
|
||||
}
|
||||
8
bitnami/node/example/public/stylesheets/style.less
Normal file
8
bitnami/node/example/public/stylesheets/style.less
Normal file
@@ -0,0 +1,8 @@
|
||||
body {
|
||||
padding: 50px;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B7FF;
|
||||
}
|
||||
9
bitnami/node/example/routes/index.js
Normal file
9
bitnami/node/example/routes/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.render('index', { title: 'Express' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
9
bitnami/node/example/routes/users.js
Normal file
9
bitnami/node/example/routes/users.js
Normal file
@@ -0,0 +1,9 @@
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
/* GET users listing. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.send('respond with a resource');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
6
bitnami/node/example/views/error.jade
Normal file
6
bitnami/node/example/views/error.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= message
|
||||
h2= error.status
|
||||
pre #{error.stack}
|
||||
5
bitnami/node/example/views/index.jade
Normal file
5
bitnami/node/example/views/index.jade
Normal file
@@ -0,0 +1,5 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= title
|
||||
p Welcome to #{title}
|
||||
7
bitnami/node/example/views/layout.jade
Normal file
7
bitnami/node/example/views/layout.jade
Normal file
@@ -0,0 +1,7 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
link(rel='stylesheet', href='/stylesheets/style.css')
|
||||
body
|
||||
block content
|
||||
Reference in New Issue
Block a user