├── .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 | --------------------------------------------------------------------------------