├── .gitignore
├── README.md
├── docker-compose.ci.build.yml
├── docker-compose.yml
├── service-a
├── Dockerfile
├── package.json
├── public
│ ├── app.css
│ ├── app.js
│ └── index.html
└── server.js
└── service-b
├── Dockerfile
├── Program.cs
├── Startup.cs
└── project.json
/.gitignore:
--------------------------------------------------------------------------------
1 | project.lock.json
2 | [Bb]in/
3 | [Oo]bj/
4 | .DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sample app for demonstrating continuous integration and deployment of a multi-container Docker app to Azure Container Service
2 | This repository contains a sample Azure multi-container Docker application.
3 |
4 | * service-a: Angular.js sample application with Node.js backend
5 | * service-b: ASP .NET Core sample service
6 |
7 | ## Run application locally
8 | First, compile the ASP .NET Core application code. This uses a container to isolate build dependencies that is also used by VSTS for continuous integration:
9 |
10 | ```
11 | docker-compose -f docker-compose.ci.build.yml run ci-build
12 | ```
13 |
14 | (On Windows, you currently need to pass the -d flag to docker-compose run and poll the container to determine when it has completed).
15 |
16 | Now build Docker images and run the services:
17 |
18 | ```
19 | docker-compose up --build
20 | ```
21 |
22 | The frontend service (service-a) will be available at http://localhost:8080.
23 |
--------------------------------------------------------------------------------
/docker-compose.ci.build.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 |
3 | services:
4 | ci-build:
5 | image: microsoft/dotnet:1.0.0-preview2.1-sdk
6 | volumes:
7 | - ./service-b:/src
8 | working_dir: /src
9 | command: /bin/bash -c "dotnet restore && dotnet publish -c Release -o bin ."
10 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 |
3 | services:
4 | service-a:
5 | build: ./service-a
6 | ports:
7 | - "8080:80"
8 | depends_on:
9 | - service-b
10 | - mycache
11 | labels:
12 | com.microsoft.acs.dcos.marathon.healthcheck.path: '/'
13 |
14 | service-b:
15 | build: ./service-b
16 | expose:
17 | - "80"
18 | labels:
19 | com.microsoft.acs.dcos.marathon.healthcheck.path: '/'
20 |
21 | mycache:
22 | image: redis:alpine
23 | expose:
24 | - "6379"
25 |
--------------------------------------------------------------------------------
/service-a/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:argon
2 | EXPOSE 80
3 |
4 | WORKDIR /app
5 | COPY package.json .
6 | RUN npm install
7 | COPY . .
8 |
9 | CMD ["node", "server.js"]
10 |
--------------------------------------------------------------------------------
/service-a/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "service-a",
3 | "version": "0.1.0",
4 | "description": "Sample service for multi-container Docker app",
5 | "dependencies": {
6 | "express": "^4.13.4",
7 | "morgan": "^1.7.0",
8 | "redis": "^2.6.3",
9 | "request": "^2.71.0"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/service-a/public/app.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin-left: 10px;
3 | margin-right: 10px;
4 | }
5 |
6 | .message {
7 | font-family: Courier New, Courier, monospace;
8 | font-weight: bold;
9 | }
10 |
--------------------------------------------------------------------------------
/service-a/public/app.js:
--------------------------------------------------------------------------------
1 | var app = angular.module('myApp', ['ngRoute']);
2 |
3 | app.controller('MainController', function($scope, $http) {
4 |
5 | $scope.messages = [];
6 | $scope.sayHelloToServer = function() {
7 | $http.get("/api?_=" + Date.now()).then(function(response) {
8 | $scope.messages.push(response.data);
9 |
10 | // Make request to /metrics
11 | $http.get("/metrics?_=" + Date.now()).then(function(response) {
12 | $scope.metrics = response.data;
13 | });
14 | });
15 | };
16 |
17 | $scope.sayHelloToServer();
18 |
19 | var styles = [];
20 | var colors = ["black", "green", "red", "blue", "orange", "purple", "gray"];
21 | var colorIndex = 0;
22 |
23 | $scope.getStyle = function(message) {
24 | if (!styles[message]) {
25 | styles[message] = {'color': colors[colorIndex]};
26 | colorIndex = colorIndex < colors.length - 1 ? colorIndex + 1 : 0;
27 | }
28 | return styles[message];
29 | }
30 |
31 | });
32 |
--------------------------------------------------------------------------------
/service-a/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
Server Says Hello
15 |
16 |
17 |
18 |
19 | {{message}}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Count: {{metrics.requestCount | number}}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/service-a/server.js:
--------------------------------------------------------------------------------
1 | var os = require('os');
2 | var request = require('request');
3 | var morgan = require('morgan');
4 | var express = require('express');
5 | var redis = connectToCache();
6 |
7 | var app = express();
8 | app.use(express.static(__dirname + '/public'));
9 | app.use(morgan("dev"));
10 |
11 | // application -------------------------------------------------------------
12 | app.get('/', function (req, res) {
13 | res.sendFile(__dirname + '/public/index.html');
14 | });
15 |
16 | // api ------------------------------------------------------------
17 | app.get('/api', function (req, res) {
18 | // Increment requestCount each time API is called
19 | if (!redis) { redis = connectToCache(); }
20 | redis.incr('requestCount', function (err, reply) {
21 | var requestCount = reply;
22 | });
23 |
24 | // Invoke service-b
25 | request('http://service-b', function (error, response, body) {
26 | res.send('Hello from service A running on ' + os.hostname() + ' and ' + body);
27 | });
28 | });
29 |
30 | app.get('/metrics', function (req, res) {
31 | if (!redis) { redis = connectToCache(); }
32 | redis.get('requestCount', function (err, reply) {
33 | res.send({ requestCount: reply });
34 | });
35 | });
36 |
37 | var port = 80;
38 | var server = app.listen(port, function () {
39 | console.log('Listening on port ' + port);
40 | });
41 |
42 | process.on("SIGINT", () => {
43 | process.exit(130 /* 128 + SIGINT */);
44 | });
45 |
46 | process.on("SIGTERM", () => {
47 | console.log("Terminating...");
48 | server.close();
49 | });
50 |
51 | function connectToCache() {
52 | var redis = require('redis').createClient("redis://mycache");
53 | return redis;
54 | }
55 |
--------------------------------------------------------------------------------
/service-b/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM microsoft/dotnet:1.0.1-runtime
2 | EXPOSE 80
3 |
4 | WORKDIR /app
5 | COPY ./bin .
6 |
7 | CMD ["dotnet", "service-b.dll"]
--------------------------------------------------------------------------------
/service-b/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using System.Runtime.Loader;
4 | using System.Threading;
5 | using Microsoft.AspNetCore.Hosting;
6 |
7 | namespace ServiceB
8 | {
9 | public class Program
10 | {
11 | public static void Main(string[] args)
12 | {
13 | using (var main = new CancellationTokenSource())
14 | using (var cts = new CancellationTokenSource())
15 | {
16 | var token = cts.Token;
17 |
18 | Console.CancelKeyPress += (sender, e) => {
19 | cts.Cancel();
20 | e.Cancel = true;
21 | };
22 |
23 | AssemblyLoadContext.GetLoadContext(typeof(Program).GetTypeInfo().Assembly).Unloading += context => {
24 | if (!cts.IsCancellationRequested)
25 | {
26 | cts.Cancel();
27 | }
28 | if (!main.IsCancellationRequested)
29 | {
30 | main.Token.WaitHandle.WaitOne();
31 | }
32 | };
33 |
34 | new WebHostBuilder()
35 | .UseKestrel(options => {
36 | options.ShutdownTimeout = TimeSpan.FromSeconds(10);
37 | })
38 | .UseStartup()
39 | .UseUrls("http://*:80")
40 | .Build()
41 | .Run(cts.Token);
42 |
43 | main.Cancel();
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/service-b/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.ApplicationInsights;
4 | using Microsoft.ApplicationInsights.AspNetCore;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.Http;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.DependencyInjection;
10 | using Microsoft.Extensions.Logging;
11 | using Microsoft.Extensions.Logging.Console;
12 |
13 | namespace ServiceB
14 | {
15 | public class Startup
16 | {
17 | public IConfigurationRoot Configuration;
18 |
19 | public Startup(IHostingEnvironment env)
20 | {
21 | var aiKey = Environment.GetEnvironmentVariable("APPINSIGHTS_INSTRUMENTATIONKEY");
22 | var devMode = Environment.GetEnvironmentVariable("APPINSIGHTS_DEVELOPER_MODE");
23 | var useDevMode = env.IsDevelopment() || !String.IsNullOrEmpty(devMode);
24 | this.Configuration = new ConfigurationBuilder()
25 | .AddApplicationInsightsSettings(
26 | instrumentationKey: aiKey,
27 | developerMode: useDevMode ? (bool?)true : null)
28 | .Build();
29 | }
30 |
31 | public void ConfigureServices(IServiceCollection services)
32 | {
33 | services.AddApplicationInsightsTelemetry(this.Configuration);
34 | }
35 |
36 | public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
37 | {
38 | loggerFactory.AddConsole();
39 | app.UseApplicationInsightsRequestTelemetry();
40 | app.UseApplicationInsightsExceptionTelemetry();
41 | app.ApplicationServices.GetService().Context.Properties["Service name"] = "service-b";
42 | app.Run(async context =>
43 | {
44 | await context.Response.WriteAsync("Hello from service B running on " + Environment.MachineName);
45 | });
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/service-b/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "service-b",
3 | "version": "1.0.0-*",
4 | "buildOptions": {
5 | "debugType": "portable",
6 | "emitEntryPoint": true
7 | },
8 | "dependencies": {},
9 | "frameworks": {
10 | "netcoreapp1.0": {
11 | "dependencies": {
12 | "Microsoft.NETCore.App": {
13 | "type": "platform",
14 | "version": "1.0.1"
15 | },
16 | "System.Runtime.Loader": "4.0.0",
17 | "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
18 | "Microsoft.ApplicationInsights.AspNetCore": "1.0.0",
19 | "Microsoft.Extensions.Logging.Console": "1.0.0"
20 | },
21 | "imports": "dnxcore50"
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------