19 | Swapping to the Development environment displays detailed information about the error that occurred.
20 |
21 |
22 | The Development environment shouldn't be enabled for deployed applications.
23 | It can result in displaying sensitive information from exceptions to end users.
24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
25 | and restarting the app.
26 |
27 |
--------------------------------------------------------------------------------
/1-container-1-domain/Dockerfile:
--------------------------------------------------------------------------------
1 | # build .NET app:
2 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0-alpine as buildnet
3 |
4 | WORKDIR /src
5 |
6 | COPY NetApi/NetApi.csproj .
7 | RUN dotnet restore
8 |
9 | COPY NetApi .
10 | RUN dotnet build -c Release
11 |
12 | # RUN dotnet test ...
13 |
14 | RUN dotnet publish -c Release -o /dist
15 |
16 |
17 | # build Vue app:
18 | FROM node:alpine as buildvue
19 |
20 | WORKDIR /src
21 |
22 | COPY vueapp/package.json .
23 | RUN npm install
24 |
25 | # webpack build
26 | COPY vueapp .
27 | RUN npm run build
28 |
29 |
30 | # Copy results from both places into production container:
31 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-alpine
32 |
33 | WORKDIR /app
34 |
35 | ENV ASPNETCORE_ENVIRONMENT Production
36 | ENV ASPNETCORE_URLS http://+:5000
37 | EXPOSE 5000
38 |
39 | # copy .net content
40 | COPY --from=buildnet /dist .
41 | # copy vue content into .net's static files folder:
42 | COPY --from=buildvue /src/dist /app/wwwroot
43 |
44 | CMD ["dotnet", "NetApi.dll"]
45 |
--------------------------------------------------------------------------------
/0-CLIs/README.md:
--------------------------------------------------------------------------------
1 | Create the Apps
2 | ===============
3 |
4 | The first step is to create the apps using each product's CLI:
5 |
6 | 1. Download .NET Core SDK from https://dotnet.microsoft.com/download/dotnet-core
7 |
8 | 2. Download Vue.js CLI from NPM using `npm` or `yarn`:
9 |
10 | `npm install -g @vue/cli`
11 |
12 | Install Node from https://nodejs.org/ if needed.
13 |
14 | 3. Scaffold the .NET project:
15 |
16 | `dotnet new webapi -n NetApi`
17 |
18 | 4. Scaffold the Vue app using the Vue cli:
19 |
20 | `vue create vueapp`
21 |
22 | This will walk you through a wizard allowing you to pick your favorite options.
23 |
24 | 5. To call the backend, we added `src/components/ApiValues.vue`, referenced it from `src/App.vue`, and created `vue.config.js` to proxy requests to the back-end.
25 |
26 |
27 | Results
28 | -------
29 |
30 | The contents of this folder are the results of doing these steps.
31 |
32 | We'll copy this content into each other scenario as we build `Dockerfile`s for each technique.
33 |
--------------------------------------------------------------------------------
/0-CLIs/vueapp/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { register } from "register-service-worker";
4 |
5 | if (process.env.NODE_ENV === "production") {
6 | register(`${process.env.BASE_URL}service-worker.js`, {
7 | ready() {
8 | console.log(
9 | "App is being served from cache by a service worker.\n" +
10 | "For more details, visit https://goo.gl/AFskqB"
11 | );
12 | },
13 | registered() {
14 | console.log("Service worker has been registered.");
15 | },
16 | cached() {
17 | console.log("Content has been cached for offline use.");
18 | },
19 | updatefound() {
20 | console.log("New content is downloading.");
21 | },
22 | updated() {
23 | console.log("New content is available; please refresh.");
24 | },
25 | offline() {
26 | console.log(
27 | "No internet connection found. App is running in offline mode."
28 | );
29 | },
30 | error(error) {
31 | console.error("Error during service worker registration:", error);
32 | }
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/0-Dockerfiles-for-each/vueapp/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { register } from "register-service-worker";
4 |
5 | if (process.env.NODE_ENV === "production") {
6 | register(`${process.env.BASE_URL}service-worker.js`, {
7 | ready() {
8 | console.log(
9 | "App is being served from cache by a service worker.\n" +
10 | "For more details, visit https://goo.gl/AFskqB"
11 | );
12 | },
13 | registered() {
14 | console.log("Service worker has been registered.");
15 | },
16 | cached() {
17 | console.log("Content has been cached for offline use.");
18 | },
19 | updatefound() {
20 | console.log("New content is downloading.");
21 | },
22 | updated() {
23 | console.log("New content is available; please refresh.");
24 | },
25 | offline() {
26 | console.log(
27 | "No internet connection found. App is running in offline mode."
28 | );
29 | },
30 | error(error) {
31 | console.error("Error during service worker registration:", error);
32 | }
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/1-container-1-domain/vueapp/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { register } from "register-service-worker";
4 |
5 | if (process.env.NODE_ENV === "production") {
6 | register(`${process.env.BASE_URL}service-worker.js`, {
7 | ready() {
8 | console.log(
9 | "App is being served from cache by a service worker.\n" +
10 | "For more details, visit https://goo.gl/AFskqB"
11 | );
12 | },
13 | registered() {
14 | console.log("Service worker has been registered.");
15 | },
16 | cached() {
17 | console.log("Content has been cached for offline use.");
18 | },
19 | updatefound() {
20 | console.log("New content is downloading.");
21 | },
22 | updated() {
23 | console.log("New content is available; please refresh.");
24 | },
25 | offline() {
26 | console.log(
27 | "No internet connection found. App is running in offline mode."
28 | );
29 | },
30 | error(error) {
31 | console.error("Error during service worker registration:", error);
32 | }
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/2-containers-1-domain/vueapp/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { register } from "register-service-worker";
4 |
5 | if (process.env.NODE_ENV === "production") {
6 | register(`${process.env.BASE_URL}service-worker.js`, {
7 | ready() {
8 | console.log(
9 | "App is being served from cache by a service worker.\n" +
10 | "For more details, visit https://goo.gl/AFskqB"
11 | );
12 | },
13 | registered() {
14 | console.log("Service worker has been registered.");
15 | },
16 | cached() {
17 | console.log("Content has been cached for offline use.");
18 | },
19 | updatefound() {
20 | console.log("New content is downloading.");
21 | },
22 | updated() {
23 | console.log("New content is available; please refresh.");
24 | },
25 | offline() {
26 | console.log(
27 | "No internet connection found. App is running in offline mode."
28 | );
29 | },
30 | error(error) {
31 | console.error("Error during service worker registration:", error);
32 | }
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/2-containers-2-domains/vueapp/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { register } from "register-service-worker";
4 |
5 | if (process.env.NODE_ENV === "production") {
6 | register(`${process.env.BASE_URL}service-worker.js`, {
7 | ready() {
8 | console.log(
9 | "App is being served from cache by a service worker.\n" +
10 | "For more details, visit https://goo.gl/AFskqB"
11 | );
12 | },
13 | registered() {
14 | console.log("Service worker has been registered.");
15 | },
16 | cached() {
17 | console.log("Content has been cached for offline use.");
18 | },
19 | updatefound() {
20 | console.log("New content is downloading.");
21 | },
22 | updated() {
23 | console.log("New content is available; please refresh.");
24 | },
25 | offline() {
26 | console.log(
27 | "No internet connection found. App is running in offline mode."
28 | );
29 | },
30 | error(error) {
31 | console.error("Error during service worker registration:", error);
32 | }
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/server-rendered-spa/README.md:
--------------------------------------------------------------------------------
1 | Server-rendered SPA
2 | ===================
3 |
4 | Server-rendering produces the SPA's first render cycle on the server, and ships this customized html to the browser. The SPA boots up on the browser, and continues the interactions from here.
5 |
6 |
7 | Technique
8 | ---------
9 |
10 | Inside Visual Studio, choose File -> New -> ASP.NET Core Website, then choose the Angular or React template. (Sadly no Vue template.) It uses the same "host both front-end and back-end in one site" solution we saw in chapter 4. In addition, it includes server-rendered content. Check the "Add Docker Support" check-box in the new project dialog, and it'll scaffold out the Dockerfile too.
11 |
12 |
13 | Results
14 | -------
15 |
16 | The SPA page is first rendered on the server, producing great SEO-worthy pages on first paint. Then the SPA boots up and continues the interaction. This is great when Search Engine Optimization is key, and works well for users with JavaScript disabled. It requires a server-platform that supports this though (usually Node.js), and generally strays far from the SPA's CLI tools.
17 |
--------------------------------------------------------------------------------
/0-CLIs/NetApi/Controllers/ValuesController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace NetApi.Controllers
8 | {
9 | [Route("api/[controller]")]
10 | [ApiController]
11 | public class ValuesController : ControllerBase
12 | {
13 | // GET api/values
14 | [HttpGet]
15 | public ActionResult> Get()
16 | {
17 | return new string[] { ".NET", "Backend" };
18 | }
19 |
20 | // GET api/values/5
21 | [HttpGet("{id}")]
22 | public ActionResult Get(int id)
23 | {
24 | return "value";
25 | }
26 |
27 | // POST api/values
28 | [HttpPost]
29 | public void Post([FromBody] string value)
30 | {
31 | }
32 |
33 | // PUT api/values/5
34 | [HttpPut("{id}")]
35 | public void Put(int id, [FromBody] string value)
36 | {
37 | }
38 |
39 | // DELETE api/values/5
40 | [HttpDelete("{id}")]
41 | public void Delete(int id)
42 | {
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/1-container-1-domain/NetApi/Controllers/ValuesController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace NetApi.Controllers
8 | {
9 | [Route("api/[controller]")]
10 | [ApiController]
11 | public class ValuesController : ControllerBase
12 | {
13 | // GET api/values
14 | [HttpGet]
15 | public ActionResult> Get()
16 | {
17 | return new string[] { ".NET", "Backend" };
18 | }
19 |
20 | // GET api/values/5
21 | [HttpGet("{id}")]
22 | public ActionResult Get(int id)
23 | {
24 | return "value";
25 | }
26 |
27 | // POST api/values
28 | [HttpPost]
29 | public void Post([FromBody] string value)
30 | {
31 | }
32 |
33 | // PUT api/values/5
34 | [HttpPut("{id}")]
35 | public void Put(int id, [FromBody] string value)
36 | {
37 | }
38 |
39 | // DELETE api/values/5
40 | [HttpDelete("{id}")]
41 | public void Delete(int id)
42 | {
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/0-Dockerfiles-for-each/NetApi/Controllers/ValuesController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace NetApi.Controllers
8 | {
9 | [Route("api/[controller]")]
10 | [ApiController]
11 | public class ValuesController : ControllerBase
12 | {
13 | // GET api/values
14 | [HttpGet]
15 | public ActionResult> Get()
16 | {
17 | return new string[] { ".NET", "Backend" };
18 | }
19 |
20 | // GET api/values/5
21 | [HttpGet("{id}")]
22 | public ActionResult Get(int id)
23 | {
24 | return "value";
25 | }
26 |
27 | // POST api/values
28 | [HttpPost]
29 | public void Post([FromBody] string value)
30 | {
31 | }
32 |
33 | // PUT api/values/5
34 | [HttpPut("{id}")]
35 | public void Put(int id, [FromBody] string value)
36 | {
37 | }
38 |
39 | // DELETE api/values/5
40 | [HttpDelete("{id}")]
41 | public void Delete(int id)
42 | {
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/2-containers-1-domain/NetApi/Controllers/ValuesController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace NetApi.Controllers
8 | {
9 | [Route("api/[controller]")]
10 | [ApiController]
11 | public class ValuesController : ControllerBase
12 | {
13 | // GET api/values
14 | [HttpGet]
15 | public ActionResult> Get()
16 | {
17 | return new string[] { ".NET", "Backend" };
18 | }
19 |
20 | // GET api/values/5
21 | [HttpGet("{id}")]
22 | public ActionResult Get(int id)
23 | {
24 | return "value";
25 | }
26 |
27 | // POST api/values
28 | [HttpPost]
29 | public void Post([FromBody] string value)
30 | {
31 | }
32 |
33 | // PUT api/values/5
34 | [HttpPut("{id}")]
35 | public void Put(int id, [FromBody] string value)
36 | {
37 | }
38 |
39 | // DELETE api/values/5
40 | [HttpDelete("{id}")]
41 | public void Delete(int id)
42 | {
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/2-containers-2-domains/NetApi/Controllers/ValuesController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace NetApi.Controllers
8 | {
9 | [Route("api/[controller]")]
10 | [ApiController]
11 | public class ValuesController : ControllerBase
12 | {
13 | // GET api/values
14 | [HttpGet]
15 | public ActionResult> Get()
16 | {
17 | return new string[] { ".NET", "Backend" };
18 | }
19 |
20 | // GET api/values/5
21 | [HttpGet("{id}")]
22 | public ActionResult Get(int id)
23 | {
24 | return "value";
25 | }
26 |
27 | // POST api/values
28 | [HttpPost]
29 | public void Post([FromBody] string value)
30 | {
31 | }
32 |
33 | // PUT api/values/5
34 | [HttpPut("{id}")]
35 | public void Put(int id, [FromBody] string value)
36 | {
37 | }
38 |
39 | // DELETE api/values/5
40 | [HttpDelete("{id}")]
41 | public void Delete(int id)
42 | {
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server-rendered-spa/WebApplication1/ClientApp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "WebApplication1",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "bootstrap": "^4.1.3",
7 | "jquery": "3.5.1",
8 | "merge": "^1.2.1",
9 | "oidc-client": "^1.6.1",
10 | "react": "^16.0.0",
11 | "react-dom": "^16.0.0",
12 | "react-router-bootstrap": "^0.24.4",
13 | "react-router-dom": "^4.2.2",
14 | "react-scripts": "^2.1.8",
15 | "reactstrap": "^6.3.0",
16 | "rimraf": "^2.6.2"
17 | },
18 | "devDependencies": {
19 | "ajv": "^6.9.1",
20 | "babel-eslint": "^9.0.0",
21 | "cross-env": "^5.2.0",
22 | "eslint": "^5.12.0",
23 | "eslint-config-react-app": "^3.0.8",
24 | "eslint-plugin-flowtype": "^3.5.1",
25 | "eslint-plugin-import": "^2.14.0",
26 | "eslint-plugin-jsx-a11y": "^5.1.1",
27 | "eslint-plugin-react": "^7.11.1"
28 | },
29 | "eslintConfig": {
30 | "extends": "react-app"
31 | },
32 | "scripts": {
33 | "start": "rimraf ./build && react-scripts start",
34 | "build": "react-scripts build",
35 | "test": "cross-env CI=true react-scripts test --env=jsdom",
36 | "eject": "react-scripts eject",
37 | "lint": "eslint ./src/"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/server-rendered-spa/WebApplication1.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.28922.388
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication1", "WebApplication1\WebApplication1.csproj", "{B54AD0D5-24DC-4B16-B242-7E44E7A9A3CA}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {B54AD0D5-24DC-4B16-B242-7E44E7A9A3CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {B54AD0D5-24DC-4B16-B242-7E44E7A9A3CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {B54AD0D5-24DC-4B16-B242-7E44E7A9A3CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {B54AD0D5-24DC-4B16-B242-7E44E7A9A3CA}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {AAC911BE-C587-4AE2-9D07-F57F508BC88B}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/server-rendered-spa/WebApplication1/Controllers/SampleDataController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace WebApplication1.Controllers
8 | {
9 | [Route("api/[controller]")]
10 | public class SampleDataController : Controller
11 | {
12 | private static string[] Summaries = new[]
13 | {
14 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
15 | };
16 |
17 | [HttpGet("[action]")]
18 | public IEnumerable WeatherForecasts()
19 | {
20 | var rng = new Random();
21 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast
22 | {
23 | DateFormatted = DateTime.Now.AddDays(index).ToString("d"),
24 | TemperatureC = rng.Next(-20, 55),
25 | Summary = Summaries[rng.Next(Summaries.Length)]
26 | });
27 | }
28 |
29 | public class WeatherForecast
30 | {
31 | public string DateFormatted { get; set; }
32 | public int TemperatureC { get; set; }
33 | public string Summary { get; set; }
34 |
35 | public int TemperatureF {
36 | get {
37 | return 32 + (int)(TemperatureC / 0.5556);
38 | }
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Docker Isn't All Hype; Create Robust Deployments for Vue.js and ASP.NET Core
2 | ============================================================================
3 |
4 | This demonstrates various approaches to building a SPA application and supporting backend in Docker. It is the companion code to the [Docker Isn't All Hype](https://robrich.org/slides/docker-vue-and-aspnetcore/#/) presentation.
5 |
6 | Vue and ASP.NET were used here purely for demonstration. The same principles apply to Angular or React, Java or Python, or any technology with a single page app and a supporting back-end.
7 |
8 | In each folder, we demonstrate one approach:
9 |
10 | - 0-CLIs - Let's scaffold out both projects.
11 |
12 | - 0-Dockerfiles-for-each - Classic Dockerfiles for each piece. Here's where most tutorials stop.
13 |
14 | - 2-containers-2-domains - We put our website on https://www.example.com/ and our api on https://api.example.com/.
15 |
16 | - 2-containers-1-domain - We put both front-end and back-end into separate containers, but stitch them together into a single URL via a Kubernetes Ingress controller.
17 |
18 | - 1-container-1-domain - Deploy both SPA and back-end into a single container, using the back-end to host the front-end's static files.
19 |
20 | - server-rendered-spa - Visual Studio's New Project template hosts both pieces together and includes server-rendered components.
21 |
22 |
23 | LICENSE: MIT, Copyright Richardson & Sons, LLC.
24 |
--------------------------------------------------------------------------------
/0-Dockerfiles-for-each/README.md:
--------------------------------------------------------------------------------
1 | Dockerfiles for each step
2 | =========================
3 |
4 | In this chapter we'll copy the content from `0-CLIs` and add the typical `Dockerfile`s for each project. When we're done, we'll have fully functional containers, but they won't interact with each other.
5 |
6 | New content since Chapter 0
7 | ---------------------------
8 |
9 | 1. `NetApi/Dockerfile` is the file used to package a .NET app, and represents a typical `Dockerfile`.
10 |
11 | 2. `vueapp/Dockerfile` is the file used to package the Vue.js app.
12 |
13 | 3. Each folder has a `.dockerignore` file. Like the `.gitignore` file, these files are not copied into the Docker image.
14 |
15 | 4. `vueapp/nginx.conf` is a config file for Nginx to serve the index.html file as the 404 page.
16 |
17 |
18 | Use it
19 | ------
20 |
21 | 1. Build the .NET image:
22 |
23 | ```bash
24 | cd NetApi
25 | docker build -t net-api:step0 .
26 | cd ..
27 | ```
28 |
29 | 2. Build the Vue.js image:
30 |
31 | ```bash
32 | cd vueapp
33 | docker build -t vue-app:step0 .
34 | cd ..
35 | ```
36 |
37 | Results
38 | -------
39 |
40 | This is the typical result we'd get if we followed a "Docker for Vue.js" and a "Docker for ASP.NET Core" tutorial. Each system is working fine, but they're not working together.
41 |
42 | Digging into each technique, we'll see how we can customize the `Dockerfile`s and applications to meet the needs of each hosting and container strategy.
43 |
--------------------------------------------------------------------------------
/server-rendered-spa/WebApplication1/ClientApp/src/components/FetchData.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export class FetchData extends Component {
4 | static displayName = FetchData.name;
5 |
6 | constructor(props) {
7 | super(props);
8 | this.state = { forecasts: [], loading: true };
9 | }
10 |
11 | componentDidMount() {
12 | this.populateWeatherData();
13 | }
14 |
15 | static renderForecastsTable(forecasts) {
16 | return (
17 |
Client-side navigation. For example, click Counter then Back to return here.
19 |
Development server integration. In development mode, the development server from create-react-app runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file.
20 |
Efficient production builds. In production mode, development-time features are disabled, and your dotnet publish configuration produces minified, efficiently bundled JavaScript files.
21 |
22 |
The ClientApp subdirectory is a standard React application based on the create-react-app template. If you open a command prompt in that directory, you can run npm commands such as npm test or npm install.
23 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/0-CLIs/NetApi/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.HttpsPolicy;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Microsoft.Extensions.Hosting;
12 | using Microsoft.Extensions.Logging;
13 |
14 | namespace NetApi
15 | {
16 | public class Startup
17 | {
18 | public Startup(IConfiguration configuration)
19 | {
20 | Configuration = configuration;
21 | }
22 |
23 | public IConfiguration Configuration { get; }
24 |
25 | // This method gets called by the runtime. Use this method to add services to the container.
26 | public void ConfigureServices(IServiceCollection services)
27 | {
28 | services.AddControllers()
29 | .AddNewtonsoftJson();
30 | }
31 |
32 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
33 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
34 | {
35 | if (env.IsDevelopment())
36 | {
37 | app.UseDeveloperExceptionPage();
38 | }
39 | else
40 | {
41 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
42 | app.UseHsts();
43 | }
44 |
45 | app.UseHttpsRedirection();
46 |
47 | app.UseRouting();
48 |
49 | app.UseAuthorization();
50 |
51 | app.UseEndpoints(endpoints =>
52 | {
53 | endpoints.MapControllers();
54 | });
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/0-Dockerfiles-for-each/NetApi/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.HttpsPolicy;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Microsoft.Extensions.Hosting;
12 | using Microsoft.Extensions.Logging;
13 |
14 | namespace NetApi
15 | {
16 | public class Startup
17 | {
18 | public Startup(IConfiguration configuration)
19 | {
20 | Configuration = configuration;
21 | }
22 |
23 | public IConfiguration Configuration { get; }
24 |
25 | // This method gets called by the runtime. Use this method to add services to the container.
26 | public void ConfigureServices(IServiceCollection services)
27 | {
28 | services.AddControllers()
29 | .AddNewtonsoftJson();
30 | }
31 |
32 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
33 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
34 | {
35 | if (env.IsDevelopment())
36 | {
37 | app.UseDeveloperExceptionPage();
38 | }
39 | else
40 | {
41 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
42 | app.UseHsts();
43 | }
44 |
45 | app.UseHttpsRedirection();
46 |
47 | app.UseRouting();
48 |
49 | app.UseAuthorization();
50 |
51 | app.UseEndpoints(endpoints =>
52 | {
53 | endpoints.MapControllers();
54 | });
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/2-containers-1-domain/NetApi/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.HttpsPolicy;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Microsoft.Extensions.Hosting;
12 | using Microsoft.Extensions.Logging;
13 |
14 | namespace NetApi
15 | {
16 | public class Startup
17 | {
18 | public Startup(IConfiguration configuration)
19 | {
20 | Configuration = configuration;
21 | }
22 |
23 | public IConfiguration Configuration { get; }
24 |
25 | // This method gets called by the runtime. Use this method to add services to the container.
26 | public void ConfigureServices(IServiceCollection services)
27 | {
28 | services.AddControllers()
29 | .AddNewtonsoftJson();
30 | }
31 |
32 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
33 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
34 | {
35 | if (env.IsDevelopment())
36 | {
37 | app.UseDeveloperExceptionPage();
38 | }
39 | else
40 | {
41 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
42 | app.UseHsts();
43 | }
44 |
45 | app.UseHttpsRedirection();
46 |
47 | app.UseRouting();
48 |
49 | app.UseAuthorization();
50 |
51 | app.UseEndpoints(endpoints =>
52 | {
53 | endpoints.MapControllers();
54 | });
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/1-container-1-domain/README.md:
--------------------------------------------------------------------------------
1 | 1 Container, 1 Domain
2 | =====================
3 |
4 | In this technique, both front-end and back-end run from the same container built from a single `Dockerfile`. We can't scale them separately, but we deploy changes in either simultaniously.
5 |
6 |
7 | New content since Chapter 0
8 | ---------------------------
9 |
10 | 1. `NetApi/Startup.cs` adds `DefaultFiles` and `StaticFiles` references so .NET can serve the JavaScript, CSS, and images.
11 |
12 | 2. `NetApi/Controllers/SpaController.cs` serves the `index.html` page for each route in the Vue app.
13 |
14 | 3. `vueapp/vue.config.js` is for development only, and hooks up Webpack development server. It looks for requests to `/api/any/path/here` and forwards it to the backend.
15 |
16 | 4. `vueapp/nginx.conf` is gone. We use .NET's Kestrel web server for that now.
17 |
18 | 5. `Dockerfile` at the root of the solution replaces the files in each project. This single file builds stages for each application.
19 |
20 |
21 | Use it: dev
22 | -----------
23 |
24 | 1. Run the backend outside the container:
25 |
26 | ```
27 | cd NetApi
28 | dotnet run
29 | ```
30 |
31 | 2. Run the frontend outside the container:
32 |
33 | ```
34 | cd vueapp
35 | npm install
36 | npm run serve
37 | ```
38 |
39 | 3. Open the browser to the frontend:
40 |
41 | `http://localhost:8080/`
42 |
43 |
44 | Use it: production
45 | ------------------
46 |
47 | 1. Build and run the single container:
48 |
49 | ```
50 | docker build -t spa-and-api:1c1d .
51 | docker run -p 5000:5000 -d spa-and-api:1c1d
52 | ```
53 |
54 | 2. Launch your browser to the app:
55 |
56 | `http://localhost:5000/`
57 |
58 |
59 | Results
60 | -------
61 |
62 | In dev, we leverage Webpack Dev Server to proxy through the front-end to the back-end. In Production, we deploy the SPA's static files into the back-end's static files directory (`public` or `wwwroot` or similar).
63 |
--------------------------------------------------------------------------------
/1-container-1-domain/NetApi/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.HttpsPolicy;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Microsoft.Extensions.Hosting;
12 | using Microsoft.Extensions.Logging;
13 |
14 | namespace NetApi
15 | {
16 | public class Startup
17 | {
18 | public Startup(IConfiguration configuration)
19 | {
20 | Configuration = configuration;
21 | }
22 |
23 | public IConfiguration Configuration { get; }
24 |
25 | // This method gets called by the runtime. Use this method to add services to the container.
26 | public void ConfigureServices(IServiceCollection services)
27 | {
28 | services.AddControllers()
29 | .AddNewtonsoftJson();
30 | }
31 |
32 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
33 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
34 | {
35 | if (env.IsDevelopment())
36 | {
37 | app.UseDeveloperExceptionPage();
38 | }
39 | else
40 | {
41 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
42 | app.UseHsts();
43 | }
44 |
45 | app.UseHttpsRedirection();
46 |
47 | app.UseDefaultFiles();
48 | app.UseStaticFiles();
49 |
50 | app.UseRouting();
51 |
52 | app.UseAuthorization();
53 |
54 | app.UseEndpoints(endpoints =>
55 | {
56 | endpoints.MapControllers();
57 | });
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/server-rendered-spa/WebApplication1/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.AspNetCore.HttpsPolicy;
4 | using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Hosting;
8 |
9 | namespace WebApplication1
10 | {
11 | public class Startup
12 | {
13 | public Startup(IConfiguration configuration)
14 | {
15 | Configuration = configuration;
16 | }
17 |
18 | public IConfiguration Configuration { get; }
19 |
20 | // This method gets called by the runtime. Use this method to add services to the container.
21 | public void ConfigureServices(IServiceCollection services)
22 | {
23 | services.AddMvc(options => options.EnableEndpointRouting = false)
24 | .AddNewtonsoftJson();
25 |
26 | // In production, the React files will be served from this directory
27 | services.AddSpaStaticFiles(configuration =>
28 | {
29 | configuration.RootPath = "ClientApp/build";
30 | });
31 | }
32 |
33 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
34 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
35 | {
36 | if (env.IsDevelopment())
37 | {
38 | app.UseDeveloperExceptionPage();
39 | }
40 | else
41 | {
42 | app.UseExceptionHandler("/Error");
43 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
44 | app.UseHsts();
45 | }
46 |
47 | app.UseHttpsRedirection();
48 | app.UseStaticFiles();
49 | app.UseSpaStaticFiles();
50 |
51 | app.UseMvc(routes =>
52 | {
53 | routes.MapRoute(
54 | name: "default",
55 | template: "{controller}/{action=Index}/{id?}");
56 | });
57 |
58 | app.UseSpa(spa =>
59 | {
60 | spa.Options.SourcePath = "ClientApp";
61 |
62 | if (env.IsDevelopment())
63 | {
64 | spa.UseReactDevelopmentServer(npmScript: "start");
65 | }
66 | });
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/2-containers-2-domains/NetApi/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.HttpsPolicy;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Microsoft.Extensions.Hosting;
12 | using Microsoft.Extensions.Logging;
13 |
14 | namespace NetApi
15 | {
16 | public class Startup
17 | {
18 |
19 | public Startup(IConfiguration configuration)
20 | {
21 | Configuration = configuration;
22 | }
23 |
24 | public IConfiguration Configuration { get; }
25 |
26 | // This method gets called by the runtime. Use this method to add services to the container.
27 | public void ConfigureServices(IServiceCollection services)
28 | {
29 | // CORS configuration for ASP.NET Core:
30 | // https://docs.microsoft.com/en-us/aspnet/core/security/cors
31 | // must not have a trailing slash
32 | string CORS_DOMAIN = Configuration.GetValue("CORS_DOMAIN");
33 | Console.WriteLine($"CORS_DOMAIN: {CORS_DOMAIN}");
34 |
35 | services.AddCors(options =>
36 | {
37 | options.AddDefaultPolicy(builder =>
38 | {
39 | builder.WithOrigins(CORS_DOMAIN);
40 | });
41 | });
42 |
43 | services.AddControllers()
44 | .AddNewtonsoftJson();
45 | }
46 |
47 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
48 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
49 | {
50 | if (env.IsDevelopment())
51 | {
52 | app.UseDeveloperExceptionPage();
53 | }
54 | else
55 | {
56 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
57 | app.UseHsts();
58 | }
59 |
60 | app.UseCors();
61 |
62 | app.UseHttpsRedirection();
63 |
64 | app.UseRouting();
65 |
66 | app.UseAuthorization();
67 |
68 | app.UseEndpoints(endpoints =>
69 | {
70 | endpoints.MapControllers();
71 | });
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/2-containers-1-domain/README.md:
--------------------------------------------------------------------------------
1 | 2 Containers, 1 Domain
2 | ======================
3 |
4 | In this technique, we assume both website and api are running on the same subdomain -- both on https://www.example.com/. Yet they're deployed as separate containers so we can scale each differently.
5 |
6 | In production, a Kubernetes [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) links the two sites together. (We could also use an Nginx or HAProxy.)
7 |
8 | In development, [Webpack](https://stackoverflow.com/a/49846977/702931) proxies content through the Webpack development server to the api.
9 |
10 | We assume here that all resources on `/api/*` are to go to the backend, and all other urls go to the frontend.
11 |
12 |
13 | New content since Chapter 0
14 | ---------------------------
15 |
16 | 1. `Kubernetes` folder contains all the files used to install both docker images into the cluster, and rig them behind the ingress controller.
17 |
18 | 2. `vueapp/vue.config.js` is for development only, and hooks up Webpack development server. It looks for requests to `/api/any/path/here` and forwards it to the backend.
19 |
20 |
21 | Use it: dev
22 | -----------
23 |
24 | 1. Run the backend outside the container:
25 |
26 | ```
27 | cd NetApi
28 | dotnet run
29 | ```
30 |
31 | 2. Run the frontend outside the container:
32 |
33 | ```
34 | cd vueapp
35 | npm install
36 | npm run serve
37 | ```
38 |
39 | 3. Open the browser to the frontend:
40 |
41 | `http://localhost:8080/`
42 |
43 |
44 | Use it: production
45 | ------------------
46 |
47 | 1. Build both the backend and frontend images:
48 |
49 | ```
50 | cd NetApi
51 | docker build -t net-api:2c1d .
52 | cd ..
53 | cd vueapp
54 | docker build -t vue-app:2c1d .
55 | cd ..
56 | ```
57 |
58 | 2. Install the kubernetes content:
59 |
60 | ```
61 | kubectl apply -f Kubernetes
62 | ```
63 |
64 | kubectl will loop through all the files, installing each one.
65 |
66 | 3. Get the IP for the ingress controller:
67 |
68 | ```
69 | kubectl get ingress spa-and-api
70 | ```
71 |
72 | 4. Launch your browser to the Kubernetes Cluster IP:
73 |
74 | `http://localhost:80/`
75 |
76 |
77 | Results
78 | -------
79 |
80 | We have the best of both worlds. We don't have the complexity of `CORS` headers or `BASE_URL` environment variables, yet we can scale each container separately. The price for this solution: we need a complex container orchestrator such as Kubernetes to proxy traffic from a single load balancer to the proper container.
81 |
--------------------------------------------------------------------------------
/server-rendered-spa/WebApplication1/WebApplication1.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.0
5 | true
6 | Latest
7 | false
8 | ClientApp\
9 | $(DefaultItemExcludes);$(SpaRoot)node_modules\**
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | %(DistFiles.Identity)
44 | PreserveNewest
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/2-containers-2-domains/README.md:
--------------------------------------------------------------------------------
1 | 2 Containers, 2 Domains
2 | =======================
3 |
4 | Let's imagine we'll deploy the SPA to https://www.example.com/ and the back-end API to https://api.example.com/
5 |
6 | Because they're separate, each can grow and evolve separately. This has some up-sides and some downsides.
7 |
8 |
9 | New content since Chapter 0
10 | ---------------------------
11 |
12 | 1. `NetApi/Dockerfile` and `NetApi/Startup.cs` have references to CORS headers using the `CORS_DOMAIN` environment variable. We can override this as we launch the container:
13 |
14 | `docker run -e CORS_DOMAIN=https://foo -p 5000:5000 -d net-api`
15 |
16 | The variable we're specifying here is the allowed domain(s) that can use this service.
17 |
18 | 2. `vueapp/.env.production` and `vueapp/src/components/ApiValues.vue` reference `VUE_APP_BASE_URL` environment variable. This is the api's domain, and gets baked into the image, so we can't override this at runtime.
19 |
20 | Because these two environment variables will be different in each environment, we need to carefully adjust these files **each time we build** to link the environments back together.
21 |
22 | Note that these URLs aren't between the containers, but rather how the browser referrs to each. This is because the browser is running the SPA, and making the call to the backend.
23 |
24 |
25 | Use it: dev
26 | -----------
27 |
28 | 1. Set the `CORS_DOMAIN` environment variable in `NetApi/Properties/launchSettings.json` or in the Properties pages inside Visual Studio.
29 |
30 | 2. Set the `VUE_APP_BASE_URL` environment variable in `vueapp/.env.local`.
31 |
32 | 3. Run the backend outside the container:
33 |
34 | ```
35 | cd NetApi
36 | dotnet run
37 | ```
38 |
39 | 4. Run the frontend outside the container:
40 |
41 | ```
42 | cd vueapp
43 | npm install
44 | npm run serve
45 | ```
46 |
47 | 5. Open the browser to the frontend:
48 |
49 | `http://localhost:8080/`
50 |
51 |
52 | Use it: production
53 | ------------------
54 |
55 | 1. Adjust the `CORS_DOMAIN` line in `NetApi/Dockerfile`.
56 |
57 | 2. Adjust the `VUE_APP_BASE_URL` line in `vueapp/.env.production`.
58 |
59 | 3. Build the .NET image:
60 |
61 | ```bash
62 | cd NetApi
63 | docker build -t net-api:2c2d .
64 | cd ..
65 | ```
66 |
67 | 4. Build the Vue.js image:
68 |
69 | ```bash
70 | cd vueapp
71 | docker build -t vue-app:2c2d .
72 | cd ..
73 | ```
74 |
75 | 5. Run both containers:
76 |
77 | ```bash
78 | docker run -p 5000:5000 -d net-api:2c2d
79 | docker run -p 3000:80 -d vue-app:2c2d
80 | ```
81 |
82 | 6. Launch your browser to the vue app:
83 |
84 | `http://localhost:3000/`
85 |
86 |
87 | Results
88 | -------
89 |
90 | Each side can grow independently, they can get deployed independently -- even to different hosting platforms or vendors. But we need to carefully configure `CORS` headers in the API, and carefully configure the API's `BASE_URL` in the front-end. This configuration must be unique per environment, and may be different for different developers depending on their tools (Docker Desktop vs Minikube vs K3s).
91 |
--------------------------------------------------------------------------------
/0-CLIs/vueapp/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ msg }}
4 |
5 | For a guide and recipes on how to configure / customize this project,
6 | check out the
7 | vue-cli documentation.
10 |