Skip to main content
  1. Blog/

SPA deployment with GitLab

·915 words·5 mins
Antonio Morrone
Author
Antonio Morrone
Staff/Tech Lead Software Engineer. Building software since 2013, in blockchain protocols & Web3 infrastructure since 2021. Security-minded. Remote from Italy.

In the last few projects, I started using GitLab, not only as a git repository server, but also as a DevOps platform. So here I am going to describe a very simple architecture for deploying a single-page application using Docker, docker-compose, and nginx. In this specific project I also used dotnet-core for the back-end API and VueJS as the 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 considered using a 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 this way they are completely independent from each other
  2. This way the project structure can be reused with any framework/language combination
  3. We are almost separating the local development phase from the DevOps phase (apart from the inclusion of the gitlab-ci.yml files).

Front-end static files generation
#

I used VueJS and the related vue-cli tools to generate the static files, but it doesn’t really matter for 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
  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 we make use of dotnet-core, we need to build the project, so this step could be unnecessary if you choose a language that doesn’t require compilation (e.g. Node.js, 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 produce 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 folder to the nginx container
  • ./api/ the folder containing the Dockerfile used to set up the dotnet APIs container.

For completeness, here is 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 intended as an 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 into the script commands:

  1. Retrieve the front-end artifacts by using the GitLab API (for non-enterprise users)
  • It also requires setting the PRIVATE-TOKEN variable with a personal access token, which 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 repository 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!