Antomor logo

Antomor's personal website.

Where ideas become words (maybe)

SPA deployment with GitLab

A small example of a single-page application deployment using GitLab, docker and nginx

antomor

5-Minute Read

Gitlab logo

In the last projects, I started using GitLab, not only as git repository server, but also as DevOps platform. So here I am going to describe a very simple architecture to deploy a single page application using Docker and docker-compose, nginx. In this specific project I used also dotnet-core for the back-end API and VueJS as front-end framework, but it is language-agnostic, meaning that you can replace whatever back-end or front-end you prefer.

First of all, following the separation of concerns, let’s consider each part on its own. I have created 3 repositories:

  1. Front-end
  2. Back-end
  3. Deploy

I have considered using one single repo to maintain all the codebase, but I ended-up with this structure for the following reasons:

  1. Often the teams working on back-end and front-end are not the same, so in this way they are completely independent from each-other
  2. In that way the project structure can be re-used with all frameworks/languages combinations
  3. We are almost separating the local development phase, with the DevOps phase (apart from the inclusion of the gitlab-ci.yml files).

Front-end static files generation

I have used the VueJS and the related vue-cli tools to generate the static files, but it doesn’t really matter in the whole application; the only important thing is to generate static files.

In our case the gitlab-ci.yml file will look like:

build site:
  image: node:latest
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist

description =

  1. It creates a job named build site
  2. It executes the script in a docker image node:latest
  3. It creates a build stage to execute the following script in the image aforementioned:
npm ci
npm run build
  1. It stores the artifacts created in the dist directory.

Back-end API build

Since that we make use of dotnet-core, we need to build the project, so this step could be unnecessary if you choose some language that doesn’t require to be compiled (e.g. NodeJs, Python, etc…).

Our gitlab-ci.yml:

build:
  image: microsoft/dotnet:sdk
  stage: build
  script:
    - dotnet restore
    - dotnet publish -c Release -o out
  artifacts:
    paths:
      - out

As you can see, the file looks very similar to the previous one.

  1. It creates a build job
  2. It uses a different docker image to build the project microsoft/dotnet:sdk
  3. It creates a build stage to execute the following scripts:
- dotnet restore  # it restores the dependencies
- dotnet publish -c Release -o out # it builds the project
  1. It stores the artifacts created in the out directory.

So at the moment, we have 2 projects that produces their own artifacts. It’s time to let them talk!

Deploy project

Project structure

|__ api
|     |__ Dockerfile
|__ nginx
|     |__ nginx.conf
|__ gitlab-ci.yml
|__ docker-compose.yml

This project performs the following steps:

  1. Retrieve files generated from the front-end
  2. Retrieve files generated from the back-end
  3. Set-up nginx

docker-compose.yml:

version: '3'
services:
  nginx: 
    image: nginx:latest
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./dist/:/var/www/html/
    ports:
      - 80:80
      - 443:443
    networks:
      - proxy-net

  web:
    build: ./api/
    networks:
      - proxy-net
networks:
  proxy-net:

From the docker-compose file it is important to notice two folders:

  • ./dist/ used to map the static file folders the nginx container
  • ./api/ the folder containing the Dockerfile used to setup the dotnet APIs container.

For completeness here the content of the Dockerfile used to run the dotnet APIs.

# Build runtime image
FROM microsoft/dotnet:aspnetcore-runtime
WORKDIR /app
COPY . .
ENTRYPOINT ["dotnet", "Api.dll"]

Where Api is the name of the dotnet API project.

nginx.conf:

events {
    worker_connections  1024;
}

http {

    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    server {
        server_name <name_of_the_server>; # TODO: to be replaced

        location / {
            # This would be the directory where your SPA static files are stored at
            root /var/www/html/;
            try_files $uri $uri/ /index.html;
        }

        location /api {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_pass http://web;
            proxy_ssl_session_reuse off;
            proxy_set_header Host $http_host;
            proxy_cache_bypass $http_upgrade;
            proxy_redirect off;
            proxy_set_header   Upgrade $http_upgrade;
            proxy_set_header   Connection keep-alive;
        }
    }
}

The previous nginx configuration is to be intended as example only. For nginx production configuration please refer to the following resources

We are assuming this project is deployed by an agent running on a machine with docker-compose installed.

Let’s have a look at gitlab-ci.yml.

deploy:
  stage: deploy
  only:
    - "master"
  environment: production
  script:
    - "curl -L --header \"PRIVATE-TOKEN: $TOKEN\" \"https://gitlab.com/api/v4/projects/14072554/jobs/artifacts/master/download?job=build+site\" --output artifacts.zip"
    - unzip artifacts.zip
    - "curl -L --header \"PRIVATE-TOKEN: $TOKEN\" \"https://gitlab.com/api/v4/projects/10080203/jobs/artifacts/master/download?job=build\" --output api.zip"
    - unzip api.zip
    - cp -R out/* api/
    - sh down.sh
    - sh up.sh
  1. It creates a deploy job
  2. It creates a deploy stage with the following script section:
  - "curl -L --header \"PRIVATE-TOKEN: $TOKEN\" \"https://gitlab.com/api/v4/projects/14072554/jobs/artifacts/master/download?job=build+site\" --output artifacts.zip"
  - unzip artifacts.zip
  - "curl -L --header \"PRIVATE-TOKEN: $TOKEN\" \"https://gitlab.com/api/v4/projects/10080203/jobs/artifacts/master/download?job=build\" --output api.zip"
  - unzip api.zip
  - cp -R out/* api/
  - docker-compose up --build -d

So let’s dive deeper in the script commands:

  1. Retrieve the front-end artifacts by using the gitlab API (for non-enterprise users)
  • It also requires to set the PRIVATE-TOKEN variable with a personal access token, that can be generated from User Settings -> Access Tokens
  1. Unzip the front-end artifacts
  2. Retrieve the back-end artifacts
  3. Unzip the back-end artifacts
  4. Run docker-compose

Conclusion

I have tried to describe only the steps necessary to deploy a single-page application using docker and GitLab, without considering the initial steps of repositories creation and GitLab runner setup.

While the setup of the GitLab runner used to deploy the application is required, the front-end and back-end builds make use of the shared runners available in GitLab.

For further details, please don’t hesitate to get in touch!

comments powered by Disqus

Recent Posts

Categories

About

Software Engineer passionate about Security and Privacy. Nature and animals lover. Sports (running, yoga, boxing) practitioner.