diff --git a/README.md b/README.md deleted file mode 100644 index abd6524..0000000 --- a/README.md +++ /dev/null @@ -1,94 +0,0 @@ - -# Node Bulletin Board - -A Node.js sample app which shows an event bulletin board, using Vue.js for the front-end and SQL Server for storage. The app runs in containers and the only pre-requisite for building and running the whole stack is Docker. - -![Bulletin Board app](img/bulletin-board.jpg) - -### Credits - -The original app is from [Vue Events Bulletin Board](https://github.com/chenkie/vue-events-bulletin) and the Prometheus integration comes from [Example Prometheus Monitoring](https://github.com/RisingStack/example-prometheus-nodejs). - -## Tech Stack - -The entrypoint to the app is an Nginx container, which acts as a reverse proxy to the Node.js application container. The application container stores data in the SQL Server container, and it also exposes instrumentation metrics which are scraped by a Prometheus container. The Prometheus metrics are exposed in a dashboard from the Grafana container. - -## Usage - -The [Docker Compose](docker-compose.yml) file uses an environment variable for your Docker ID, so you can build the images and push to Docker Hub. Start by capturing your Docker ID in an environment variable: - -``` -export dockerId='' -``` - -### Build Docker images - -Build the application and database images using Docker Compose: - -``` -docker-compose build -``` - -### Run the app - -You can run the app using the same compose file: - -``` -docker-compose up -d -``` - -Browse to [localhost](http://localhost) to use the app. - -### Configure Grafana - -The compose file runs Prometheus to collect metrics and Grafana to show an application dashbaord. Grfana needs some additional setup. - -Browse to [localhost:3000](http://localhost:3000) and log in to Grafana with the credentials `admin` / `admin`. - -Add a new data source with the following details: - -- Name: **prometheus** - -- Type: **Prometheus** - -- URL: **http://bb-metrics:9090** - -![Grafana data source](img/grafana-data-source.jpg) - -From the Grafana icon, click _Dashboards... Import_ and load the JSON dashboard file from [bulletin-board-dashboard/dashboard.json](bulletin-board-dashboard/dashboard.json). Select the Prometheus data store. - -You'll now see the application dashboard - send some load into the app by refreshing the browser, and the graphs will be populated: - -![Grafana dashboard](img/grafana-dashboard.jpg) - -> You can save the configured Grafana container as an image, which persists all the setup changes. - -``` -docker container commit nodebulletinboard_bb-dashboard_1 $dockerId/bulletin-board-dashboard -``` - -### Running in swarm mode - -You can deploy the app to a Docker swarm for high availability and scale. A single-node swarm is fine for testing. - -If you're running the app from compose, first remove all the containers: - -``` -docker-compose down -``` - -Switch to swarm mode: - -``` -docker swarm init -``` - -> If your Docker host has multiple IP addresses, you'll need to specify the local network IP in the `advertise-addr` option. - -Then deploy the application as a stack: - -``` -docker stack deploy -c docker-stack.yml bb -``` - -You can browse to the app at the server address, and to the configured Grafana instance at port 3000. \ No newline at end of file diff --git a/bulletin-board-app/Dockerfile b/bulletin-board-app/Dockerfile index 6571625..f24e52d 100644 --- a/bulletin-board-app/Dockerfile +++ b/bulletin-board-app/Dockerfile @@ -9,4 +9,4 @@ CMD [ "npm", "start" ] COPY . . -HEALTHCHECK CMD curl --fail http://localhost:8080 || exit 1 \ No newline at end of file +HEALTHCHECK --interval=5s CMD curl --fail http://localhost:8080 || exit 1 \ No newline at end of file diff --git a/bulletin-board-dashboard/Dockerfile b/bulletin-board-dashboard/Dockerfile deleted file mode 100644 index f93f9f8..0000000 --- a/bulletin-board-dashboard/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM grafana/grafana:4.6.2 - -ENV GF_PATHS_DATA='/data' - -WORKDIR /data diff --git a/bulletin-board-dashboard/dashboard.json b/bulletin-board-dashboard/dashboard.json deleted file mode 100644 index 465fe2a..0000000 --- a/bulletin-board-dashboard/dashboard.json +++ /dev/null @@ -1,613 +0,0 @@ -{ - "__inputs": [ - { - "name": "DS_PROMETHEUS", - "label": "prometheus", - "description": "", - "type": "datasource", - "pluginId": "prometheus", - "pluginName": "Prometheus" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "4.6.2" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "" - }, - { - "type": "datasource", - "id": "prometheus", - "name": "Prometheus", - "version": "1.0.0" - }, - { - "type": "panel", - "id": "singlestat", - "name": "Singlestat", - "version": "" - } - ], - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - }, - { - "datasource": "${DS_PROMETHEUS}", - "enable": false, - "expr": "ALERTS", - "hide": false, - "iconColor": "rgba(255, 96, 96, 1)", - "limit": 100, - "name": "Alerts", - "showIn": 0, - "step": "10s", - "type": "alert" - } - ] - }, - "description": "Bulletin Board", - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "hideControls": false, - "id": null, - "links": [], - "refresh": "5s", - "rows": [ - { - "collapse": false, - "height": 250, - "panels": [ - { - "cacheTimeout": null, - "colorBackground": false, - "colorValue": true, - "colors": [ - "rgba(50, 172, 45, 0.97)", - "rgba(237, 129, 40, 0.89)", - "rgba(245, 54, 54, 0.9)" - ], - "datasource": "${DS_PROMETHEUS}", - "decimals": null, - "format": "none", - "gauge": { - "maxValue": 1, - "minValue": 0, - "show": true, - "thresholdLabels": true, - "thresholdMarkers": true - }, - "hideTimeOverride": false, - "id": 6, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "span": 3, - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false - }, - "tableColumn": "Value", - "targets": [ - { - "expr": "sum(increase(http_request_duration_ms_count{code=~\"^5..$\"}[1m])) / sum(increase(http_request_duration_ms_count[1m]))", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "", - "refId": "A", - "step": 20 - } - ], - "thresholds": "0.1", - "title": "Error rate", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "avg" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "id": 1, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [ - { - "type": "dashboard" - } - ], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "span": 9, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "sum(rate(http_request_duration_ms_count[1m])) by (service, route, method, code) * 60", - "format": "time_series", - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{service}} - {{method}} {{route}} {{code}}", - "metric": "", - "refId": "A", - "step": 2 - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Throughput", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "rpm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "repeat": null, - "repeatIteration": null, - "repeatRowId": null, - "showTitle": true, - "title": "Throughput", - "titleSize": "h6" - }, - { - "collapse": false, - "height": 250, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "id": 4, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.5, sum(rate(http_request_duration_ms_bucket[1m])) by (le, service, route, method))", - "format": "time_series", - "intervalFactor": 2, - "legendFormat": "{{service}} - {{method}} {{route}}", - "refId": "A", - "step": 2 - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Median Response Time", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "id": 2, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_ms_bucket[1m])) by (le, service, route, method))", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "{{service}} - {{method}} {{route}}", - "refId": "A", - "step": 2 - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "95th Response Time", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "transparent": false, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "repeat": null, - "repeatIteration": null, - "repeatRowId": null, - "showTitle": true, - "title": "Response time", - "titleSize": "h6" - }, - { - "collapse": false, - "height": 305, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "id": 3, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "avg(nodejs_external_memory_bytes / 1024) by (service)", - "format": "time_series", - "intervalFactor": 2, - "legendFormat": "{{service}}", - "refId": "A", - "step": 2 - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Memory usage", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "decmbytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "id": 7, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "avg(process_cpu_seconds_total) by (service)", - "format": "time_series", - "intervalFactor": 2, - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "CPU usage", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "repeat": null, - "repeatIteration": null, - "repeatRowId": null, - "showTitle": true, - "title": "Compute stats", - "titleSize": "h6" - } - ], - "schemaVersion": 14, - "style": "dark", - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-5m", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "timezone": "browser", - "title": "Bulletin Board", - "version": 3 -} \ No newline at end of file diff --git a/bulletin-board-metrics/Dockerfile b/bulletin-board-metrics/Dockerfile deleted file mode 100644 index 7618f95..0000000 --- a/bulletin-board-metrics/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM prom/prometheus:v2.0.0 - -COPY prometheus.yml /etc/prometheus/prometheus.yml \ No newline at end of file diff --git a/bulletin-board-metrics/prometheus.yml b/bulletin-board-metrics/prometheus.yml deleted file mode 100644 index 4f312bc..0000000 --- a/bulletin-board-metrics/prometheus.yml +++ /dev/null @@ -1,7 +0,0 @@ -global: - scrape_interval: 5s - -scrape_configs: - - job_name: 'bb-app' - static_configs: - - targets: ['bb-app:8080'] \ No newline at end of file diff --git a/bulletin-board-proxy/nginx.conf b/bulletin-board-proxy/nginx.conf index 1e34abd..9264b38 100644 --- a/bulletin-board-proxy/nginx.conf +++ b/bulletin-board-proxy/nginx.conf @@ -25,7 +25,6 @@ http { map $sent_http_content_type $expires { default off; text/css 1M; - text/javascript 1M; application/javascript 1M; application/x-font-woff 3M; ~image/ 6M; @@ -41,6 +40,7 @@ http { proxy_pass http://bb-app:8080/; proxy_cache STATIC; proxy_cache_valid 200 5s; + proxy_ignore_headers Expires Cache-Control; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; } } diff --git a/docker-compose.yml b/docker-compose.yml index 40e18e8..fec4bb3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,14 +3,14 @@ version: '3.3' services: bb-db: - image: ${dockerId}/bulletin-board-db + image: ${dockerId}/bulletin-board-db:v4 build: context: ./bulletin-board-db networks: - bb-net bb-app: - image: ${dockerId}/bulletin-board-app + image: ${dockerId}/bulletin-board-app:v4 build: context: ./bulletin-board-app depends_on: @@ -20,7 +20,7 @@ services: - bb-net bb-proxy: - image: ${dockerId}/bulletin-board-proxy + image: ${dockerId}/bulletin-board-proxy:v4 build: context: ./bulletin-board-proxy ports: @@ -30,25 +30,5 @@ services: networks: - bb-net - bb-metrics: - image: ${dockerId}/bulletin-board-metrics - build: - context: ./bulletin-board-metrics - depends_on: - - bb-app - networks: - - bb-net - - bb-dashboard: - image: ${dockerId}/bulletin-board-dashboard - build: - context: ./bulletin-board-dashboard - ports: - - "3000:3000" - depends_on: - - bb-metrics - networks: - - bb-net - networks: bb-net: \ No newline at end of file diff --git a/docker-stack.yml b/docker-stack.yml index 297cf83..665cccb 100644 --- a/docker-stack.yml +++ b/docker-stack.yml @@ -3,17 +3,17 @@ version: '3.3' services: bb-db: - image: ${dockerId}/bulletin-board-db + image: ${dockerId}/bulletin-board-db:v4 networks: - bb-net bb-app: - image: ${dockerId}/bulletin-board-app + image: ${dockerId}/bulletin-board-app:v4 networks: - bb-net bb-proxy: - image: ${dockerId}/bulletin-board-proxy + image: ${dockerId}/bulletin-board-proxy:v4 ports: - "80:80" networks: @@ -22,17 +22,5 @@ services: mode: replicated replicas: 3 - bb-metrics: - image: ${dockerId}/bulletin-board-metrics - networks: - - bb-net - - bb-dashboard: - image: ${dockerId}/bulletin-board-dashboard - ports: - - "3000:3000" - networks: - - bb-net - networks: bb-net: \ No newline at end of file diff --git a/img/bulletin-board.jpg b/img/bulletin-board.jpg deleted file mode 100644 index b692791..0000000 Binary files a/img/bulletin-board.jpg and /dev/null differ diff --git a/img/grafana-dashboard.jpg b/img/grafana-dashboard.jpg deleted file mode 100644 index 9a15793..0000000 Binary files a/img/grafana-dashboard.jpg and /dev/null differ diff --git a/img/grafana-data-source.jpg b/img/grafana-data-source.jpg deleted file mode 100644 index 1bc0543..0000000 Binary files a/img/grafana-data-source.jpg and /dev/null differ