├── api ├── .gitignore ├── .dockerignore ├── README.md ├── Dockerfile ├── main.go └── main_test.go ├── front ├── .dockerignore ├── src │ ├── index.css │ ├── index.js │ ├── App.test.js │ ├── App.css │ ├── assets │ │ └── images │ │ │ ├── nginx-logo.svg │ │ │ ├── react-logo.svg │ │ │ ├── docker-logo.svg │ │ │ ├── kubernetes-logo.svg │ │ │ └── gopher.svg │ ├── App.js │ └── registerServiceWorker.js ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── .editorconfig ├── Dockerfile ├── .gitignore ├── package.json └── README.md ├── logo.png ├── .makefile-utils.sh ├── Dockerfile.prod ├── docker-compose.prod.yml ├── docker-compose.yml ├── docker-compose.override.yml ├── deployments ├── front.yml └── api.yml ├── .circleci └── config.yml ├── nginx └── site.conf ├── bin └── production.sh ├── Makefile └── README.md /api/.gitignore: -------------------------------------------------------------------------------- 1 | /tmp -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | tmp -------------------------------------------------------------------------------- /front/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/docker-experiments/HEAD/logo.png -------------------------------------------------------------------------------- /.makefile-utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function image_exists() { 4 | docker images -q $1 5 | } -------------------------------------------------------------------------------- /front/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/docker-experiments/HEAD/front/public/favicon.ico -------------------------------------------------------------------------------- /front/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | # to match prettier defaults 6 | indent_style = space 7 | indent_size = 2 -------------------------------------------------------------------------------- /Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | COPY ./front/build /usr/share/nginx/html 4 | 5 | COPY ./nginx/site.conf /etc/nginx/conf.d/default.conf 6 | 7 | EXPOSE 80 8 | 9 | # CMD ["nginx"] -------------------------------------------------------------------------------- /front/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /front/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # api 2 | 3 | ## Development 4 | 5 | Once you `docker-compose up -d`, the whole stack spins up and your api server will run inside a container which will expose the port 5000. 6 | 7 | ## Test 8 | 9 | To run the unit tests, run the following command: 10 | 11 | ```shell 12 | docker-compose run --rm api go test -run '' 13 | ``` -------------------------------------------------------------------------------- /front/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | 3 | ARG NODE_ENV=development 4 | ENV NODE_ENV=$NODE_ENV 5 | 6 | RUN cd /usr 7 | RUN mkdir -p front 8 | 9 | # Set a working directory 10 | WORKDIR /usr/front 11 | 12 | COPY ./package.json . 13 | COPY ./package-lock.json . 14 | 15 | RUN npm install 16 | 17 | CMD ["npm", "start"] 18 | 19 | EXPOSE 3000 -------------------------------------------------------------------------------- /front/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /front/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | 3 | services: 4 | nginx: 5 | image: "topheman/docker-experiments_nginx:1.0.1" 6 | build: 7 | context: . 8 | dockerfile: ./Dockerfile.prod 9 | ports: 10 | - "80:80" 11 | # Already copied in Dockerfile - we map them to simplify debugging 12 | volumes: 13 | - ./front/build:/usr/share/nginx/html 14 | - ./nginx/site.conf:/etc/nginx/conf.d/default.conf 15 | depends_on: 16 | - api -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | 3 | services: 4 | # no ports exposed by default, (api is proxied through docker networks) 5 | # in development, it's exposed on port 5000, see docker-composed.override.yml 6 | api: 7 | image: topheman/docker-experiments_api_production:1.0.1 8 | build: 9 | target: production 10 | context: ./api 11 | # the container will restart the api if it exits with code > 0 (won't work in dev cause of fresh) 12 | restart: on-failure -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | 3 | services: 4 | api: 5 | image: topheman/docker-experiments_api_development:1.0.1 6 | command: sh -c "echo \"development\" && go get github.com/pilu/fresh && fresh" 7 | build: 8 | target: builder 9 | volumes: 10 | - ./api:/go/src/github.com/topheman/docker-experiments/api 11 | ports: 12 | # only expose in development (in production, the api server will be proxied via nginx) 13 | - "5000:5000" 14 | front: 15 | image: topheman/docker-experiments_front_development:1.0.1 16 | build: 17 | context: ./front 18 | ports: 19 | - 3000:3000 20 | volumes: 21 | - ./front:/usr/front 22 | - front-deps:/usr/front/node_modules 23 | 24 | volumes: 25 | front-deps: -------------------------------------------------------------------------------- /deployments/front.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: docker-experiments-front-deployment 5 | spec: 6 | template: 7 | metadata: 8 | labels: 9 | app: docker-experiments 10 | tier: front 11 | spec: 12 | containers: 13 | - image: topheman/docker-experiments_nginx:1.0.1 14 | name: nginx 15 | lifecycle: 16 | preStop: 17 | exec: 18 | command: ["/usr/sbin/nginx","-s","quit"] 19 | --- 20 | apiVersion: v1 21 | kind: Service 22 | metadata: 23 | name: docker-experiments-front-service 24 | spec: 25 | selector: 26 | app: docker-experiments 27 | tier: front 28 | ports: 29 | - protocol: TCP 30 | port: 80 31 | targetPort: 80 32 | type: LoadBalancer -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front", 3 | "version": "1.0.1", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.18.0", 7 | "react": "^16.4.1", 8 | "react-dom": "^16.4.1", 9 | "react-scripts": "^1.1.4" 10 | }, 11 | "scripts": { 12 | "docker-start": "docker-compose up -d", 13 | "docker-build": "docker-compose run --rm front npm run build", 14 | "docker-test": "npm run -s docker-test:unit", 15 | "docker-test:unit": "docker-compose run --rm -e CI=true front npm run -s test", 16 | "docker-test:unit:watch": "docker-compose run --rm front npm run -s test", 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "proxy": { 23 | "/api": { 24 | "target": "http://api:5000", 25 | "changeOrigin": true, 26 | "pathRewrite": { 27 | "^/api": "" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | machine: 5 | # We need the latest version of docker-engine because we use docker-compose file format v3.4 6 | # For the compatibility between docker-engine version and docker-compose file format, see https://docs.docker.com/compose/compose-file/#compose-and-docker-compatibility-matrix 7 | # For the list of available circleci images, see https://circleci.com/docs/2.0/configuration-reference/#machine 8 | image: circleci/classic:201808-01 9 | steps: 10 | - checkout 11 | - run: 12 | name: "Infos" 13 | command: | 14 | docker-compose --version 15 | docker version 16 | - run: 17 | name: "Build development images" 18 | command: docker-compose build 19 | - run: 20 | name: "Test front" 21 | command: docker-compose run --rm -e CI=true front npm run -s test 22 | - run: 23 | name: "Test api" 24 | command: docker-compose run --rm api go test -run '' -------------------------------------------------------------------------------- /front/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-logo:hover { 11 | animation: App-logo-spin infinite 1.5s ease-in-out; 12 | } 13 | 14 | .App-header { 15 | background-color: #222; 16 | height: 150px; 17 | padding: 20px; 18 | color: white; 19 | } 20 | 21 | .App-github-link a { 22 | color: white; 23 | } 24 | 25 | @media screen and (max-width: 500px) { 26 | .App-logo { 27 | height: 43px; 28 | } 29 | } 30 | 31 | .App-title { 32 | font-size: 1.5em; 33 | } 34 | 35 | .App-intro { 36 | font-size: large; 37 | } 38 | 39 | .App-button { 40 | cursor: pointer; 41 | color: white; 42 | background: black; 43 | border: 1px solid black; 44 | padding: 5px; 45 | border-radius: 5px; 46 | font-size: 1rem; 47 | } 48 | 49 | .App-button:hover { 50 | color: black; 51 | background: white; 52 | } 53 | 54 | @keyframes App-logo-spin { 55 | from { transform: rotate(0deg); } 56 | to { transform: rotate(360deg); } 57 | } 58 | -------------------------------------------------------------------------------- /deployments/api.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: docker-experiments-api-deployment 5 | spec: 6 | replicas: 2 7 | template: 8 | metadata: 9 | labels: 10 | app: docker-experiments 11 | tier: api 12 | track: stable 13 | spec: 14 | containers: 15 | - name: docker-experiments-api-container 16 | image: topheman/docker-experiments_api_production:1.0.1 17 | ports: 18 | - name: golang # Can be referred in services 19 | containerPort: 5000 # Not hostPort (we only expose the container inside the pod) 20 | --- 21 | apiVersion: v1 22 | kind: Service 23 | metadata: 24 | # named like the docker-compose service "api" 25 | # so that nginx config stays same between docker-compose and kubernetes 26 | name: api 27 | spec: 28 | selector: 29 | app: docker-experiments 30 | tier: api 31 | ports: 32 | - protocol: TCP 33 | port: 5000 34 | targetPort: golang 35 | # default type (only exposed to the pods - not to the outside, like LoadBalancer) 36 | type: ClusterIP -------------------------------------------------------------------------------- /nginx/site.conf: -------------------------------------------------------------------------------- 1 | upstream api { 2 | # reference to kubernetes/docker-compose service 3 | server api:5000; 4 | } 5 | 6 | # simple nginx configuration file 7 | server { 8 | listen 80; 9 | 10 | root /usr/share/nginx/html; 11 | 12 | # Proxy api 13 | location /api { 14 | rewrite ^/api/(.*)$ /$1 break; 15 | proxy_set_header Host $host; 16 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 17 | proxy_pass_header Set-Cookie; 18 | add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; 19 | proxy_pass http://api/; 20 | } 21 | 22 | # Serve front 23 | location / { 24 | gzip_static on; 25 | expires max; 26 | add_header Cache-Control public; 27 | add_header ETag ""; 28 | } 29 | 30 | # service-worker should not be cached by browser - https://github.com/topheman/docker-experiments/blob/master/front/README.cra.md#offline-first-considerations 31 | location /service-worker.js { 32 | # kill cache 33 | add_header Last-Modified $date_gmt; 34 | add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; 35 | if_modified_since off; 36 | expires off; 37 | etag off; 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /front/src/assets/images/nginx-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 13 | 15 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.10.3-alpine as builder 2 | 3 | # Volumes and commands may be overriden in docker-compose*.yml files. 4 | # To build the image in production mode without docker-compose, run: 5 | # `docker build ./api -t topheman/docker-experiments_api_production` 6 | # `docker run -d -p 5000:5000 topheman/docker-experiments_api_production` 7 | ENV APP_ENV "development" 8 | 9 | # alpine comes without git. We need it for go get 10 | RUN apk update && apk upgrade && apk add --no-cache git 11 | 12 | COPY . /go/src/github.com/topheman/docker-experiments/api 13 | WORKDIR /go/src/github.com/topheman/docker-experiments/api 14 | 15 | RUN go get -v ./ 16 | RUN go build 17 | 18 | # With the following target, we create a small image for production: 19 | # - no need for golang compiler / tools 20 | # - only copy the binary that was compiled at the previous step 21 | # We don't use this target in development 22 | # Infos: https://medium.com/@chemidy/create-the-smallest-and-secured-golang-docker-image-based-on-scratch-4752223b7324 23 | 24 | FROM alpine as production 25 | 26 | ENV APP_ENV "production" 27 | 28 | COPY --from=builder /go/bin/api /usr/bin/api 29 | 30 | CMD echo "production"; api 31 | 32 | # The Dockerfile should still be usable without docker-compose, so, by default, 33 | # it's a production image, the CMD will run the binary that was compiled 34 | # If you create a development image, the `command` specified in the docker-compose.override.yml 35 | # will override and use pilu/fresh which manages building and restarting dev server when sources change 36 | # Infos: https://medium.com/@craigchilds94/hot-reloading-go-programs-with-docker-glide-fresh-d5f1acb63f72 37 | 38 | # CMD echo "APP_ENV=$APP_ENV"; \ 39 | # if [ "$APP_ENV" = "production" ]; \ 40 | # then \ 41 | # api; \ 42 | # else \ 43 | # go get github.com/pilu/fresh && \ 44 | # fresh; \ 45 | # fi 46 | 47 | EXPOSE 5000 -------------------------------------------------------------------------------- /bin/production.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | USERNAME="topheman" 3 | DOCKER_IMAGE_PREFIX="$USERNAME/docker-experiments_" 4 | DOCKER_IMAGE_NAME_API_PRODUCTION=$DOCKER_IMAGE_PREFIX"api_production" 5 | DOCKER_IMAGE_NAME_FRONT_DEVELOPMENT=$DOCKER_IMAGE_PREFIX"front_development" 6 | 7 | isImageBuilt() { 8 | local output=$(docker images $1 | grep $USERNAME) 9 | if [[ -z "$output" ]]; then 10 | echo "[Check] $1: missing 😕" 11 | return 1 12 | else 13 | echo "[Check] $1: exists 👍" 14 | return 0 15 | fi 16 | } 17 | 18 | checkAllImages() { 19 | local exitCode=0 20 | isImageBuilt $DOCKER_IMAGE_NAME_API_PRODUCTION 21 | exitCode=`expr $exitCode + $?` 22 | isImageBuilt $DOCKER_IMAGE_NAME_FRONT_DEVELOPMENT 23 | exitCode=`expr $exitCode + $?` 24 | return $exitCode 25 | } 26 | 27 | build() { 28 | echo "[Build] Building JavaScript bundle inside front container" 29 | docker-compose run --rm front npm run build 30 | if [[ $? -gt 0 ]]; then 31 | echo "[Fail] Frontend bundling failed" 32 | return 1 33 | fi 34 | return 0 35 | } 36 | 37 | help() { 38 | echo -e " 39 | ⚠️ Still in progress ... 40 | 41 | \033[1mDescription\033[0m 42 | This script ensures that the docker images necessary for the production are available, 43 | then it will build the frontend bundle and inject it in the nginx production image 44 | and configure that image to reverse proxy /api to serve the backend production container. 45 | 46 | \033[1mOptions\033[0m 47 | --help 48 | Displays help (default) 49 | --build 50 | Will build the production image / bundle frontend 51 | " 52 | } 53 | 54 | if [[ $@ = *"--build"* ]]; then 55 | echo "[Check] Verifying all images" 56 | checkAllImages 57 | if [[ $? -gt 0 ]]; then 58 | echo "[Fail] Some image is missing - aborting 🚫" 59 | exit 1 60 | fi 61 | build 62 | if [[ $? -gt 0 ]]; then 63 | echo "[Fail] Build fail - aborting 🚫" 64 | exit 2 65 | fi 66 | else 67 | help 68 | exit 69 | fi -------------------------------------------------------------------------------- /api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "runtime" 10 | "time" 11 | 12 | "github.com/gorilla/mux" 13 | ) 14 | 15 | var startTime time.Time 16 | 17 | func uptime() float64 { 18 | return time.Since(startTime).Seconds() 19 | } 20 | 21 | type infos struct { 22 | Hostname string `json:"hostname"` 23 | Version string `json:"version"` 24 | Cpus int `json:"cpus"` 25 | AppEnv string `json:"APP_ENV"` 26 | Uptime float64 `json:"uptime"` 27 | } 28 | 29 | func makeInfos() *infos { 30 | hostname, err := os.Hostname() 31 | if err != nil { 32 | hostname = "Unknown" 33 | } 34 | return &infos{hostname, runtime.Version(), runtime.NumCPU(), os.Getenv("APP_ENV"), uptime()} 35 | } 36 | 37 | func rootRouteHTML(w http.ResponseWriter, _ *http.Request) { 38 | infos := makeInfos() 39 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 40 | fmt.Fprintf(w, `

Welcome

41 | `, infos.Hostname, infos.Version, infos.Cpus, infos.AppEnv) 47 | } 48 | 49 | func rootRouteJSON(w http.ResponseWriter, r *http.Request) { 50 | infos := makeInfos() 51 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 52 | json.NewEncoder(w).Encode(infos) 53 | } 54 | 55 | // only here to demonstrate the resilience of containers restarted on failure by docker-compose/kubernetes 56 | // DON'T DO THAT ON PRODUCTION 57 | func exitRoute(w http.ResponseWriter, r *http.Request) { 58 | os.Exit(1) 59 | } 60 | 61 | // MakeRouter Public router factory - this is unit tested 62 | func MakeRouter() *mux.Router { 63 | router := mux.NewRouter() 64 | router.HandleFunc("/", rootRouteJSON).Methods("GET").HeadersRegexp("Accept", "application/json") 65 | router.HandleFunc("/", rootRouteJSON).Methods("GET").Queries("format", "json") 66 | router.HandleFunc("/", rootRouteHTML).Methods("GET") 67 | router.HandleFunc("/exit", exitRoute).Methods("GET") 68 | return router 69 | } 70 | 71 | func init() { 72 | startTime = time.Now() 73 | } 74 | 75 | func main() { 76 | router := MakeRouter() 77 | log.Fatal(http.ListenAndServe(":5000", router)) 78 | } 79 | -------------------------------------------------------------------------------- /front/README.md: -------------------------------------------------------------------------------- 1 | # front 2 | 3 | This is the frontend part, based on create-react-app. The CRA readme is accessible [here](README.cra.md). 4 | 5 | ## Development 6 | 7 | Once you `docker-compose up -d`, the whole stack spins up and your webpack server will run inside a container which will expose the port 3000. 8 | 9 | ## Tasks 10 | 11 | npm tasks (such as `npm test`) are run inside the container. To avoid having to write each time `docker-compose run front npm run my-task`, I added `docker-*` tasks that you can trigger from your host machine: 12 | 13 | * `npm run docker-test`: runs all tests in a container 14 | * `npm run docker-test:unit`: runs all unit tests in a container 15 | * `npm run docker-test:unit:watch`: runs all unit tests in a container in watch mode 16 | * `npm run docker-build`: runs `npm run build` in a container (which will create the `./build` folder on the host machine) 17 | 18 | ## Manage dependencies 19 | 20 | As you can see in [docker-compose.override.yml](../docker-compose.override.yml), the `node_modules` folder is binded to the named volume `front-deps`, so that those will be persisted outside of the container (but without mapping to a local `node_modules` folder of the host). 21 | 22 | Keep in mind that the `npm install` should be done from the container (based on Linux), because the OS of your host might be something else (like Windows or Mac OS) - native modules relying on C will need to be compiled according to your OS. 23 | 24 | * `docker-compose run front npm install`: will run `npm install` from inside the container 25 | * `docker-compose run front sh -c "rm -rf ./node_modules/*"`: will clear the `node_modules` folder from inside the container (you can't just remove it like `rm -rf ./node_modules`, you will have an error since the volume is mounted from the container) 26 | * `docker-compose run front sh -c "rm -rf ./node_modules/* && npm install"`: clear `node_modules` and run `npm install` 27 | 28 | ## Miscellaneous 29 | 30 | ### Proxy 31 | 32 | In development mode, we use the create-react-app's [proxy](#proxying-api-requests-in-development) to proxy calls from `/api` to `http://api:5000` (the requests go from the `front` container to the `api` container via the docker subnet, without your host machine having access and are expose on `http://localhost:3000/api`). Check the `proxy` section of the [`package.json`](package.json). -------------------------------------------------------------------------------- /api/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestRouter(t *testing.T) { 11 | // create a test server using our router 12 | router := MakeRouter() 13 | ts := httptest.NewServer(router) 14 | defer ts.Close() 15 | 16 | makeRequest := func(method string, path string, headers map[string]string, body io.Reader) *http.Request { 17 | req, err := http.NewRequest(method, ts.URL+path, body) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | if len(headers) > 0 { 22 | for key, value := range headers { 23 | req.Header.Set(key, value) 24 | } 25 | } 26 | return req 27 | } 28 | 29 | tests := []struct { 30 | name string 31 | req *http.Request 32 | assert func(title string, res *http.Response) 33 | }{ 34 | { 35 | name: "with(Accept:application/json)", 36 | req: makeRequest("GET", "/", map[string]string{"Accept": "application/json"}, nil), 37 | assert: func(title string, res *http.Response) { 38 | expectedContentType := "application/json; charset=utf-8" 39 | contentType := res.Header.Get("Content-Type") 40 | if contentType != expectedContentType { 41 | t.Error(title, "Wrong Content-Type header", "| expected:", expectedContentType, "| received:", contentType) 42 | } 43 | }, 44 | }, 45 | { 46 | name: "with(?format=json)", 47 | req: makeRequest("GET", "/?format=json", nil, nil), 48 | assert: func(title string, res *http.Response) { 49 | expectedContentType := "application/json; charset=utf-8" 50 | contentType := res.Header.Get("Content-Type") 51 | if contentType != expectedContentType { 52 | t.Error(title, "Wrong Content-Type header", "| expected:", expectedContentType, "| received:", contentType) 53 | } 54 | }, 55 | }, 56 | { 57 | name: "with(default)", 58 | req: makeRequest("GET", "/", nil, nil), 59 | assert: func(title string, res *http.Response) { 60 | expectedContentType := "text/html; charset=utf-8" 61 | contentType := res.Header.Get("Content-Type") 62 | if contentType != expectedContentType { 63 | t.Error(title, "Wrong Content-Type header", "| expected:", expectedContentType, "| received:", contentType) 64 | } 65 | }, 66 | }, 67 | } 68 | 69 | for _, test := range tests { 70 | t.Run(test.name, func(t *testing.T) { 71 | res, err := http.DefaultClient.Do(test.req) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | test.assert(test.name, res) 76 | defer res.Body.Close() 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /front/src/assets/images/react-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /front/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import reactLogo from './assets/images/react-logo.svg'; 3 | import dockerLogo from './assets/images/docker-logo.svg'; 4 | import nginxLogo from './assets/images/nginx-logo.svg'; 5 | import kubernetesLogo from './assets/images/kubernetes-logo.svg'; 6 | import gopher from './assets/images/gopher.svg'; 7 | import './App.css'; 8 | import axios from 'axios'; 9 | 10 | const CancelToken = axios.CancelToken; 11 | const source = CancelToken.source(); 12 | 13 | class App extends Component { 14 | state = { 15 | loading: false, 16 | error: null, 17 | data: null 18 | } 19 | componentDidMount() { 20 | this.fetchInfos() 21 | } 22 | componentWillUnmount() { 23 | source.cancel({ 24 | type: "componentWillUnmount", 25 | message:"Component unmounting, cancelling inflight requests" 26 | }); 27 | } 28 | fetchInfos() { 29 | this.setState({ 30 | error: null, 31 | loading: true, 32 | data: null 33 | }); 34 | axios.get('/api', { cancelToken: source.token }).then(result => { 35 | this.setState({ 36 | data: result.data, 37 | loading: false 38 | }) 39 | }) 40 | .catch(error => { 41 | if (axios.isCancel(error) && error.message && error.message.type === "componentWillUnmount") { 42 | console.error(error.message.message); 43 | } 44 | else { 45 | this.setState({ 46 | error, 47 | loading: false 48 | }); 49 | } 50 | }) 51 | } 52 | exit() { 53 | axios.get('/api/exit'); 54 | } 55 | render() { 56 | const {loading, error, data} = this.state; 57 | return ( 58 |
59 |
60 | kubernetes logo 61 | docker logo 62 | react logo 63 | nginx logo 64 | golang logo 65 |

docker-experiments

66 |

sources on github

67 |
68 |

Basic infos retrieved from the dockerized golang api:

69 | {loading &&

Loading ...

} 70 | {error &&

An error occured

} 71 | {data &&
    {Object.entries(data).map(([key, value]) =>
  • 72 | {key}: {key !== 'uptime' ? value : `${parseInt(value, 10)}s`} 73 |
  • )}
} 74 |

75 | 76 |

77 | {process.env.NODE_ENV === 'production' &&

78 | this.exit()} className="App-button">exit 1 the api server and see the container restart the api server on failure, thanks to docker-compose or kubernetes (reload infos to check uptime). 79 |

} 80 |
81 | ); 82 | } 83 | } 84 | 85 | export default App; 86 | -------------------------------------------------------------------------------- /front/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | UTILS := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))/.makefile-utils.sh 2 | 3 | OK_COLOR = \033[0;32m 4 | NO_COLOR = \033[m 5 | 6 | DOCKER_USER = topheman 7 | DOCKER_IMAGE_PREFIX = $(DOCKER_USER)/docker-experiments 8 | DOCKER_IMAGE_NAME_FRONT_DEV = $(DOCKER_IMAGE_PREFIX)_front_development 9 | DOCKER_IMAGE_NAME_API_DEV = $(DOCKER_IMAGE_PREFIX)_api_development 10 | DOCKER_IMAGE_NAME_API_PROD = $(DOCKER_IMAGE_PREFIX)_api_production 11 | DOCKER_IMAGE_NAME_NGINX = $(DOCKER_IMAGE_PREFIX)_nginx 12 | 13 | TAG_LATEST = latest 14 | TAG ?= 1.0.1 15 | 16 | # development docker-compose 17 | COMPOSE = docker-compose 18 | COMPOSE_RUN = $(COMPOSE) run --rm 19 | COMPOSE_RUNCI = $(COMPOSE_RUN) -e CI=true 20 | COMPOSE_RUN_FRONT = $(COMPOSE_RUN) front 21 | COMPOSE_RUNCI_FRONT = $(COMPOSE_RUNCI) front 22 | COMPOSE_RUN_API = $(COMPOSE_RUN) api 23 | 24 | # production docker-compose 25 | COMPOSEPROD = docker-compose -f ./docker-compose.yml -f ./docker-compose.prod.yml 26 | 27 | # kubernetes 28 | KUBECTL_CONFIG = -f ./deployments/api.yml -f ./deployments/front.yml 29 | 30 | default: help 31 | 32 | .PHONY: build-front-assets dev-logs-api dev-logs-front dev-logs dev-ps dev-start-d dev-start dev-stop docker-build-prod docker-images-clean docker-images-id docker-images-name docker-images kube-ps kube-start-no-rebuild kube-start kube-stop prod-logs-api prod-logs-front prod-logs prod-ps prod-start-d-no-rebuild prod-start-d prod-start-no-rebuild prod-start prod-stop test-api test-front test 33 | 34 | # rename ? 35 | build-front-assets: ## Build frontend assets into ./front/build folder 36 | $(COMPOSE_RUN_FRONT) npm run build 37 | 38 | test-front: ## Test react frontend 39 | $(COMPOSE_RUNCI_FRONT) npm run -s test 40 | 41 | test-api: ## Test golang backend 42 | $(COMPOSE_RUN_API) go test -run '' 43 | 44 | test: test-front test-api ## 🌡 Test both frontend and backend 45 | 46 | docker-images: ## List project's docker images 47 | @docker images --filter=reference='$(DOCKER_IMAGE_PREFIX)*' 48 | 49 | docker-images-name: ## List project's docker images formatted as : 50 | @docker images --format "{{.Repository}}:{{.Tag}}" --filter=reference='$(DOCKER_IMAGE_PREFIX)*' 51 | 52 | docker-images-id: ## List project's docker images formatted as 53 | @docker images --quiet --filter=reference='$(DOCKER_IMAGE_PREFIX)*' 54 | 55 | docker-images-clean: ## Clean dangling images (tagged as ) 56 | docker rmi $(shell docker images -q --filter="dangling=true") 57 | 58 | docker-build-prod: ## Build production images 59 | $(MAKE) build-front-assets 60 | docker build ./api -t $(DOCKER_IMAGE_NAME_API_PROD):$(TAG) 61 | docker build . -f Dockerfile.prod -t $(DOCKER_IMAGE_NAME_NGINX):$(TAG) 62 | 63 | dev-start: ## 🐳 Start development stack 64 | $(COMPOSE) up 65 | dev-start-d: ## Start development stack (in daemon mode) 66 | $(COMPOSE) up -d 67 | dev-stop: ## Stop development stack 68 | $(COMPOSE) down 69 | dev-ps: ## List development stack active containers 70 | $(COMPOSE) ps 71 | dev-logs: ## 🐳 Follow ALL logs (dev) 72 | $(COMPOSE) logs -f 73 | dev-logs-front: ## Follow front logs (dev) 74 | $(COMPOSE) logs -f front 75 | dev-logs-api: ## Follow api logs (dev) 76 | $(COMPOSE) logs -f api 77 | 78 | prod-start: ## 🐳 Start production stack (bundles frontend before) 79 | $(MAKE) build-front-assets 80 | $(COMPOSEPROD) up --build 81 | prod-start-d: ## Start production stack (in daemon mode) 82 | $(MAKE)build-front-assets 83 | $(COMPOSEPROD) up --build -d 84 | prod-start-no-rebuild: ## 🐳 Start production stack without recreating docker images 85 | $(COMPOSEPROD) up 86 | prod-start-d-no-rebuild: ## Start production stack (in daemon mode) without recreating docker images 87 | $(COMPOSEPROD) up -d 88 | prod-stop: ## Stop production stack 89 | $(COMPOSEPROD) down 90 | prod-ps: ## List production stack active containers 91 | $(COMPOSEPROD) ps 92 | prod-logs: ## 🐳 Follow ALL logs (prod) 93 | $(COMPOSEPROD) logs -f 94 | prod-logs-front: ## Follow front logs (prod) 95 | $(COMPOSEPROD) logs -f front 96 | prod-logs-api: ## Follow api logs (prod) 97 | $(COMPOSEPROD) logs -f api 98 | 99 | kube-start-no-rebuild: ## Create kubernetes deployment without recreating docker images 100 | kubectl create $(KUBECTL_CONFIG) 101 | kube-start: ## ☸️ Create kubernetes deployment with fresh docker images 102 | $(MAKE) docker-build-prod 103 | @echo "" 104 | $(MAKE) kube-start-no-rebuild 105 | @echo "\nYou may use $(OK_COLOR)make kube-start-no-rebuild$(NO_COLOR) next time to avoid rebuilding images each time\n" 106 | kube-stop: ## Delete kubernetes deployment with fresh docker images 107 | kubectl delete $(KUBECTL_CONFIG) 108 | kube-ps: ## List kubernetes pods and services 109 | kubectl get pods,services 110 | 111 | help: 112 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 113 | 114 | list-phony: 115 | # List all the tasks to add the to .PHONY (choose between inlined and linefeed) 116 | # bash variables are expanded with $$ 117 | # make|sed 's/\|/ /'|awk '{printf "%s+ ", $1}' 118 | # make|sed 's/\|/ /'|awk '{print $1}' 119 | @$(MAKE) help|sed 's/\|/ /'|awk '{printf "%s ", $$1}' 120 | @echo "\n" 121 | @$(MAKE) help|sed 's/\|/ /'|awk '{print $$1}' 122 | 123 | # deprecated - example of how to call a function from an other .sh file 124 | image-exists: 125 | @. $(UTILS); image_exists $(DOCKER_IMAGE_NAME_NGINX):$(TAG) -------------------------------------------------------------------------------- /front/src/assets/images/docker-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 14 | 15 | 21 | 22 | 23 | 27 | 31 | 35 | 39 | 43 | 47 | 51 | 55 | 59 | 60 | 62 | 66 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /front/src/assets/images/kubernetes-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 63 | 65 | 73 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-experiments 2 | 3 | [![CircleCI](https://circleci.com/gh/topheman/docker-experiments/tree/master.svg?style=svg)](https://circleci.com/gh/topheman/docker-experiments) 4 | 5 |

6 | 7 | This started as a simple use case to discover `docker` and `docker-compose` 🐳 : 8 | 9 | * A [front](front) made with create-react-app, running in a nodejs container for development 10 | * A very simple [api](api) made in go (the challenge is also not to have everything in JavaScript) 11 | 12 | I also setup **deployments on a local kubernetes** ☸️ and tests are running on [CircleCI](https://circleci.com/gh/topheman/docker-experiments) on each push. 13 | 14 | ## TL;DR 15 | 16 | You are a true developer? You don't RTFM? After all, this is why we have docker ... not to bother with all the boring setup/install steps ... 😉 17 | 18 | ```shell 19 | git clone https://github.com/topheman/docker-experiments.git 20 | cd docker-experiments 21 | docker-compose up -d 22 | ``` 23 | 24 | You are good to go with a development server running at [http://localhost:3000](http://localhost:3000), with the front in react, the api in go and everything hot reloading. 👏 25 | 26 | Try to take a few minutes to read the doc bellow ... 😇 27 | 28 | ## Summary 29 | 30 | * [Prerequisites](#prerequisites) 31 | * [Setup](#setup) 32 | * [Development 🛠](#development) 33 | * [Tests 🌡](#tests) 34 | * [Production - docker-compose 🐳](#production---docker-compose) 35 | * [Deployment - kubernetes ☸️](#deployment---kubernetes) 36 | * [Notes 📋](#notes) 37 | * [Docker Multi-stage builds](#docker-multi-stage-builds) 38 | * [Docker networks / Kubernetes services](#docker-networks--kubernetes-services) 39 | * [Restart on failure](#restart-on-failure) 40 | * [Commands](#commands) 41 | * [Docker commands](#docker-commands) 42 | * [Kubernetes commands](#kubernetes-commands) 43 | * [FAQ](#faq) 44 | * [CircleCI](#circleci) 45 | * [How to use latest version of docker-compose / docker-engine](#how-to-use-latest-version-of-docker-compose--docker-engine) 46 | * [Docker vs Machine executors](#docker-vs-machine-executors) 47 | * [Why does /api fallbacks to index.html in production](#why-does-api-fallbacks-to-indexhtml-in-production) 48 | * [What's next?](#whats-next) 49 | * [Resources](#resources) 50 | * [Author](#author) 51 | 52 | ## Prerequisites 53 | 54 | You need to have installed: 55 | 56 | * docker / docker-compose 57 | * npm / node (optional) 58 | * local kubernetes server and client (only if you want to play with kubernetes deployment - more about that on the [deployment section](#deployment---kubernetes)) 59 | 60 | ## Setup 61 | 62 | ```shell 63 | git clone https://github.com/topheman/docker-experiments.git 64 | ``` 65 | 66 | A [Makefile](Makefile) is available that automates all the commands that are described bellow. For each section, you'll find the related commands next to the 🖊 emoji. 67 | 68 | Just run `make help` to see the whole list. 69 | 70 | ## Development 71 | 72 | ### Launch development 73 | 74 | ```shell 75 | docker-compose up -d 76 | ``` 77 | 78 | This will create (if not already done) and launch a whole development stack, based on [docker-compose.yml](docker-compose.yml), [docker-compose.override.yml](docker-compose.override.yml), [api/Dockerfile](api/Dockerfile) and [front/Dockerfile](front/Dockerfile) - following images: 79 | 80 | * `topheman/docker-experiments_front_development`: for react development (based on nodejs image) 81 | * `topheman/docker-experiments_api_development`: for golang in development mode (using [fresh](https://github.com/pilu/fresh) to build and restart the go webserver when you change the sources) 82 | * The `services.api.command` entry in [docker-compose.override.yml](docker-compose.override.yml) will override the default `RUN` command and start a dev server (instead of running the binary compiled in the container at build time) 83 | 84 | Go to http://localhost:3000/ to access the frontend, you're good to go, the api is accessible at http://localhost:5000/. 85 | 86 | 🖊 `make dev-start`, `make dev-start-d`, `make dev-stop`, `make dev-ps`, `make dev-logs`, `make dev-logs-front`, `make dev-logs-api` 87 | 88 | ## Tests 89 | 90 | ### Launch tests 91 | 92 | ```shell 93 | docker-compose run --rm -e CI=true front npm run -s test && docker-compose run --rm api go test -run '' 94 | ``` 95 | 96 | 🖊 `make test`, `make test-front`, `make test-api` 97 | 98 | ## Production - docker-compose 99 | 100 | This section is about **testing the production images with docker-compose** 🐳 (check the [deployment section](#deployment---kubernetes) to deploy with kubernetes locally). 101 | 102 | Make sure you have built the frontend with `docker-compose run --rm front npm run build`, then: 103 | 104 | ```shell 105 | docker-compose -f ./docker-compose.yml -f ./docker-compose.prod.yml up --build 106 | ``` 107 | 108 | Note: make sure to use the `--build` flag so that it will rebuild the images if anything changed (in the source code or whatever), thanks to [docker images layers](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/), only changes will be rebuilt, based on cache (not the whole image). 109 | 110 | This will create (if not already done) and launch a whole production stack: 111 | 112 | * No nodejs image (it should not be shipped to production, the development image is only used to launch the container that creates the build artefacts with create-react-app). 113 | * `topheman/docker-experiments_api_production`: for the golang server (with the app compiled) - containing only the binary of the golang app (that way the image) 114 | * `topheman/docker-experiments_nginx`: which will: 115 | * serve the frontend (copied from `/front/build`) 116 | * proxy `/api` requests to `http://api:5000` (the docker subnet exposed by the golang api container) 117 | 118 | Access [http://localhost](http://localhost) and you're good to go. 119 | 120 | 🖊 `make prod-start`, `make prod-start-d`, `make prod-start-no-rebuild`, `make prod-start-d-no-rebuild`, `make prod-stop`, `make prod-ps`, `make prod-logs`, `make prod-logs-front`, `make prod-logs-api` 121 | 122 | ## Deployment - kubernetes 123 | 124 | This section is about **deploying the app locally with kubernetes** ☸️ (not tested with a cloud provider). To stay simple, there aren't any TLS termination management (only port 80 exposed). 125 | 126 | Local kubernetes server and client: 127 | 128 | * If you have the latest docker for Mac, both are [shipping with kubernetes](https://blog.docker.com/2018/01/docker-mac-kubernetes/) 129 | * Otherwise, you can use [minikube](https://github.com/kubernetes/minikube) for the server and install [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/) (kubernetes client) 130 | 131 | The files descripting the deployments are stored in the [deployments](deployments) folder. You will find two files, each containing the deployment and the service. 132 | 133 | ### Deploy with kubernetes 134 | 135 | 1) If you haven't built the frontend, run `docker-compose run --rm front npm run build` 136 | 137 | 2) Build the production images: 138 | 139 | ```shell 140 | docker build ./api -t topheman/docker-experiments_api_production:1.0.1 141 | docker build . -f Dockerfile.prod -t topheman/docker-experiments_nginx:1.0.1 142 | ``` 143 | 144 | Note: They are tagged `1.0.1`, same version number as in the deployments files (want to put an other version number ? Don't forget to update the deployment files). For the moment, I'm not using [Helm](http://helm.readthedocs.io/en/latest/generate-and-template/) that let's you do string interpolation on yml files. 145 | 146 | 3) Create your pods and services 147 | 148 | Make sure nothing is up on port `80`, then: 149 | 150 | ```shell 151 | kubectl create -f ./deployments/api.yml -f ./deployments/front.yml 152 | ``` 153 | 154 | You're good to go, check out [http://localhost](http://localhost) 155 | 156 | To stop and delete the pods/services you created: 157 | 158 | ```shell 159 | kubectl delete -f ./deployments/api.yml -f ./deployments/front.yml 160 | ``` 161 | 162 | They won't stop right away, you can list them and see their status with: 163 | 164 | ```shell 165 | kubectl get pods,services 166 | ``` 167 | 168 | [More commands](#kubernetes-commands) 169 | 170 | 🖊 `make kube-start`, `make kube-start-no-rebuild`, `make kube-stop`, `make kube-ps` 171 | 172 | ## Notes 173 | 174 | ### Docker Multi-stage builds 175 | 176 | Thanks to [docker multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/), the golang application is built in a docker golang:alpine image (which contains all the tooling for golang such as compiler/libs ...) and produces a small image with only a binary in an alpine image (small Linux distrib). 177 | 178 | The targets for multi-stage build are specified in the `docker*.yml` config files. 179 | 180 | The [api/Dockerfile](api/Dockerfile) will create such a production image by default. 181 | 182 | You can tell the difference of weight: 183 | 184 | ``` 185 | docker images 186 | topheman/docker-experiments_api_production latest 01f1b575fae6 About a minute ago 11.5MB 187 | topheman/docker-experiments_api_development latest fff1ef3ec29e 8 minutes ago 426MB 188 | topheman/docker-experiments_front_development latest 4ed3aea602ef 22 hours ago 225MB 189 | ``` 190 | 191 | ### Docker networks / Kubernetes services 192 | 193 | In development, the api server in golang is available at [http://localhost:5000](http://localhost:5000) and proxied onto [http://localhost:3000/api](http://localhost:3000/api) (the same port as the front, thanks to create-react-app [proxy](front/README.md#proxy)). 194 | 195 | In **production** mode, we only want the golang server to be available via `/api` (we don't want to expose it on it's own port). 196 | 197 | To make it work: 198 | 199 | * the docker-compose golang api service is named `api` - [see docker-compose.yml](docker-compose.yml). 200 | * the kubernetes services exposing the api is also named `api` - [see deployments/api.yml](deployments/api.yml) 201 | 202 | That way, the nginx conf can work with both docker-compose AND kubernetes, proxying `http://api` - [see nginx/site.conf](nginx/site.conf). 203 | 204 | ### Restart on failure 205 | 206 | If your app exits with a failure code (greater than 0) inside the container, you'll want it to restart (like you would do with pm2 and node apps). 207 | 208 | With [docker-compose/production](#production---docker-compose), the, directive `restart: on-failure` in the [docker-compose.yml](docker-compose.yml) file will ensure that. You'll be able to check it by clicking on the "exit 1 the api server" button, which will exit the golang api. You'll see that the uptime is back counting from 0 seconds. 209 | 210 | With [kubernetes/deployment](#deployment---kubernetes), I setup 2 replicas of the api server, so when you retrieve the infos, the hostname might change according of the api pod you're balance on. 211 | 212 | Exiting one pod won't break the app, it will fallback on the remaining replica. If you exit the two pods, you'll get an error retrieving infos, until one of the pod is back up by kubernetes (check their status with `kubectl get pods`). 213 | 214 | ### Commands 215 | 216 | #### Docker commands 217 | 218 | * `docker-compose run --rm front npm run test`: launch a front container in *development* mode and run tests 219 | * `docker-compose -f ./docker-compose.yml run --rm api `: launch an api container in *production* mode and run `` 220 | * `docker-compose down`: stop and remove containers, networks, volumes, and images created by `docker-compose up` 221 | 222 | Don't want to use `docker-compose` (everything bellow is already specified in the `docker*.yml` files - only dropping to remember the syntax for the futur) ? 223 | 224 | * `docker build ./api -t topheman/docker-experiments_api_production:1.0.1`: build the `api` and tag it as `topheman/docker-experiments_api_production:1.0.1` based on [api/Dockerfile](api/Dockerfile) 225 | * `docker run -d -p 5000:5000 topheman/docker-experiments_api_production:1.0.1`: runs the `topheman/docker-experiments_api_production:1.0.1` image previously created in daemon mode and exposes the ports 226 | * `docker build ./front -t topheman/docker-experiments_front_development:1.0.1`: build the `front` and tag it as `topheman/docker-experiments_front_development:1.0.1` based on [front/Dockerfile](front/Dockerfile) 227 | * `docker run --rm -p 3000:3000 -v $(pwd)/front:/usr/front -v front-deps:/usr/front/node_modules topheman/docker-experiments_front_development:1.0.1`: 228 | * runs the `topheman/docker-experiments_front_development:1.0.1` image previously created in attach mode 229 | * exposes the port 3000 230 | * creates (if not exists) and bind the volumes 231 | * the container will be removed once you kill the process (`--rm`) 232 | * `docker rmi $(docker images -q --filter="dangling=true")`: remove dangling images (layers that have no more relationships to any tagged image. Tagged as , they no longer serve a purpose and consume disk space) 233 | 234 | #### Kubernetes commands 235 | 236 | [kubectl Cheat Sheet](https://kubernetes.io/docs/reference/kubectl/cheatsheet/) 237 | 238 | * `kubectl create -f ./deployments/api.yml -f ./deployments/front.yml`: creates the resources specified in the declaration files 239 | * `kubectl delete -f ./deployments/api.yml -f ./deployments/front.yml`: deletes resources specified in the declaration files 240 | * `kubectl scale --replicas=3 deployment/docker-experiments-api-deployment`: scales up the api through 3 pods 241 | 242 | ## FAQ 243 | 244 | ### CircleCI 245 | 246 | #### How to use latest version of docker-compose / docker-engine 247 | 248 | I had the following error on my [first build](https://circleci.com/gh/topheman/docker-experiments/2): 249 | 250 | > ERROR: Version in "./docker-compose.yml" is unsupported. You might be seeing this error because you're using the wrong Compose file version. Either specify a supported version ("2.0", "2.1", "3.0", "3.1", "3.2") and place your service definitions under the `services` key, or omit the `version` key and place your service definitions at the root of the file to use version 1. 251 | > 252 | > For more on the Compose file format versions, see https://docs.docker.com/compose/compose-file/ 253 | 254 | The reason was because I'm using **docker-compose file format v3.4**, which doesn't seem to be supported by the version of docker-engine used on the default setup of CircleCI - [see compatibility matrix](https://docs.docker.com/compose/compose-file/#compose-and-docker-compatibility-matrix). 255 | 256 | With CircleCI, in **machine executor mode**, you can change/customize the image your VM will be running (by default: `circleci/classic:latest`) - see the [list of images available](https://circleci.com/docs/2.0/configuration-reference/#machine). I simply changed the image to use: 257 | 258 | ```diff 259 | version: 2 260 | jobs: 261 | build: 262 | - machine: true 263 | + machine: 264 | + image: circleci/classic:201808-01 265 | ``` 266 | 267 | Checkout [.circleci/config.yml](.circleci/config.yml) 268 | 269 | Note: Why use docker-compose file format v3.4 ? To take advantage of the `target` attribute. 270 | 271 | #### Docker vs Machine executors 272 | 273 | > You can not build Docker within Docker. 274 | 275 | To build/push docker images, you have two solutions on CircleCI: 276 | 277 | * Use the [machine executor mode](https://circleci.com/docs/2.0/executor-types/#using-machine): your jobs will be run in a dedicated, ephemeral Virtual Machine (VM) - so, you can directly run docker inside 278 | * Use the [setup_remote_docker](https://circleci.com/docs/2.0/building-docker-images/#overview) key: a remote environment will be created, and your current primary container will be configured to use it. Then, any docker-related commands you use will be safely executed in this new environment 279 | 280 | ### Why does /api fallbacks to index.html in production 281 | 282 | #### Service Worker 283 | 284 | create-react-app ships with a service worker by default which implementation is based on [sw-precache-webpack-plugin](https://github.com/goldhand/sw-precache-webpack-plugin) (a Webpack plugin that generates a service worker using [sw-precache](https://github.com/GoogleChromeLabs/sw-precache) that will cache webpack's bundles' emitted assets). 285 | 286 | It means that a `service-worker.js` file will be created at build time, listing your public static assets that the service worker will cache using a [cache first](https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#cache-falling-back-to-network) strategy (on a request for an asset, will first hit the service worker cache and serve it, then call the network and update the cache - this makes the app fast and offline-first). 287 | 288 | From the [create-react-app doc](front/README.cra.md#serving-apps-with-client-side-routing): 289 | 290 | > On a production build, and in a browser that supports [service workers](https://developers.google.com/web/fundamentals/getting-started/primers/service-workers), 291 | the service worker will automatically handle all navigation requests, like for 292 | `/todos/42` or `/api`, by serving the cached copy of your `index.html`. This 293 | service worker navigation routing can be configured or disabled by 294 | [`ejecting`](front/README.cra.md#npm-run-eject) and then modifying the 295 | [`navigateFallback`](https://github.com/GoogleChrome/sw-precache#navigatefallback-string) 296 | and [`navigateFallbackWhitelist`](https://github.com/GoogleChrome/sw-precache#navigatefallbackwhitelist-arrayregexp) 297 | options of the `SWPreachePlugin` [configuration](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/config/webpack.config.prod.js). 298 | 299 | ## What's next? 300 | 301 | The next thing that will be comming are: 302 | 303 | * [x] setup CI 304 | * [x] using [nginx](https://www.nginx.com/) as a reverse-proxy to: 305 | * [x] serve the golang api which is in its own container on `/api` 306 | * [x] make a build of the front and serve it at the root 307 | * [x] use [kubernetes](https://kubernetes.io/) to automate deployment 308 | * [x] start by playing in local with [minikube](https://github.com/kubernetes/minikube) 309 | * [ ] add [Helm](http://helm.readthedocs.io/en/latest/generate-and-template/) to do string interpolation in yaml description files ? 310 | * [ ] linting / formatting + pre-commit hooks 311 | * [ ] add linting / formatting support (eslint/prettier) with advanced config for the JavaScript part 312 | * [ ] back it up with pre-commit hooks (husky ?) 313 | * The challenge being: 314 | * any npm dependency is currently installed on a volume mounted inside the front container (not accessible by host) 315 | * how to elegantly have lint/formatting task also running on host (for vscode plugins for example but also npm tasks like), without relying on global modules (this would be cheating 😉 + we should not assume anything about the computer our project is cloned on) 316 | * how to elegantly share pre-commit hooks ? (using husky would mean an `npm install` at the root of the project) 317 | 318 | *This is still in progress*. 319 | 320 | ## Resources 321 | 322 | * Docker 323 | * 📺 💯 [Better understand containers by coding one from scratch by Liz Rice](https://twitter.com/topheman/status/1014936620309647361) 324 | * 📺 [Create a Development Environment with Docker Compose by Mark Ranallo](https://www.youtube.com/watch?v=Soh2k8lCXCA) 325 | * 📺 [Rapid Development With Docker Compose](https://www.youtube.com/watch?v=o6SScget37w) 326 | * [Golang and Docker for development and production](https://medium.com/statuscode/golang-docker-for-development-and-production-ce3ad4e69673) - use [pilu/fresh](https://github.com/pilu/fresh) to rebuild on changes in development 327 | * [Create the smallest and secured golang docker image based on scratch](https://medium.com/@chemidy/create-the-smallest-and-secured-golang-docker-image-based-on-scratch-4752223b7324) 328 | * [docker-compose with multi-stage build target (official doc)](https://docs.docker.com/compose/compose-file/#target) 329 | * Kubernetes 330 | * 📺 [Learn Kubernetes by CoderJourney](https://www.youtube.com/playlist?list=PLbG4OyfwIxjFE5Ban_n2JdGad4EDWmisR) 331 | * [Source code](https://github.com/coderjourney/meal_plan) 332 | * 📺 [Run Kubernetes Locally Using Minikube by CoderJourney](https://coderjourney.com/run-kubernetes-locally-using-minikube/) 333 | * [Setup bash completion for `kubectl`](https://twitter.com/topheman/status/1022939077602156546) 334 | 335 | More bookmarks from my research: 336 | 337 | * Docker 338 | * [Generic Docker Makefile](https://github.com/mvanholsteijn/docker-makefile) 339 | * [Awesome-docker - A curated list of Docker resources and projects](https://awesome-docker.netlify.com/) 340 | * Kubernetes 341 | * [Kubernetes & Traefik 101— When Simplicity Matters](https://dev.to/geraldcroes/kubernetes--traefik-101-when-simplicity-matters-6k6) 342 | * [Tutorial : Getting Started with Kubernetes with Docker on Mac](https://rominirani.com/tutorial-getting-started-with-kubernetes-with-docker-on-mac-7f58467203fd) 343 | * [kubernetes/dashboard](https://github.com/kubernetes/dashboard) 344 | * [Kubernetes Ingress](https://medium.com/@cashisclay/kubernetes-ingress-82aa960f658e) 345 | * [Setting up Nginx Ingress on Kubernetes](https://hackernoon.com/setting-up-nginx-ingress-on-kubernetes-2b733d8d2f45) 346 | * [Advanced kubernetes ingress](https://koudingspawn.de/advanced-ingress/) 347 | * [Kubernetes NodePort vs LoadBalancer vs Ingress? When should I use what?](https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0) 348 | * [Kubectl apply vs kubectl create?](https://stackoverflow.com/questions/47369351/kubectl-apply-vs-kubectl-create) 349 | * [awesome-kubernetes - A curated list for awesome kubernetes sources](https://ramitsurana.github.io/awesome-kubernetes/) 350 | * [Tutoriel Linux : Makefile](https://youtu.be/2VV9FAQWHdw) 351 | 352 | ## Author 353 | 354 | [Christophe Rosset](https://github.com/topheman) -------------------------------------------------------------------------------- /front/src/assets/images/gopher.svg: -------------------------------------------------------------------------------- 1 | 2 | Gopher 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 | 44 | 45 | 46 | --------------------------------------------------------------------------------