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:
- Front-end
- Back-end
- Deploy
I considered using a single repo to maintain all the codebase, but I ended up with this structure for the following reasons:
- Often the teams working on back-end and front-end are not the same, so this way they are completely independent from each other
- This way the project structure can be reused with any framework/language combination
- We are almost separating the local development phase from the DevOps phase (apart from the inclusion of the
gitlab-ci.ymlfiles).
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- It creates a job named
build site - It executes the script in a docker image
node:latest - It creates a
buildstage to execute the following script in the image aforementioned:
npm ci
npm run build- It stores the artifacts created in the
distdirectory.
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:
- outAs you can see, the file looks very similar to the previous one.
- It creates a
buildjob - It uses a different docker image to build the project
microsoft/dotnet:sdk - It creates a
buildstage to execute the following scripts:
- dotnet restore # it restores the dependencies
- dotnet publish -c Release -o out # it builds the project- It stores the artifacts created in the
outdirectory.
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.ymlThis project performs the following steps:
- Retrieve files generated from the front-end
- Retrieve files generated from the back-end
- 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 theDockerfileused 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- It creates a
deployjob - It creates a
deploystage 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 -dSo let’s dive deeper into the script commands:
- Retrieve the front-end artifacts by using the GitLab API (for non-enterprise users)
- It also requires setting the
PRIVATE-TOKENvariable with a personal access token, which can be generated fromUser Settings -> Access Tokens
- Unzip the front-end artifacts
- Retrieve the back-end artifacts
- Unzip the back-end artifacts
- 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!
