Craft a complete GitLab pipeline for Angular. Part 2
Learn Gitlab to build a CI/CD pipeline for Angular apps and libraries. This second article focuses on deployment using two methods involving GitLab Registries and Pages. You'll also find docker jobs optimization tips using custom images.

Craft a complete GitLab pipeline for Angular. Part 2
Learn Gitlab to build a CI/CD pipeline for Angular apps and libraries. This second article focuses on deployment using two methods involving GitLab Registries and Pages. You'll also find docker jobs optimization tips using custom images.


This article is part of a series of two. In the first article, you learned GitLab pipelines basics. Also, you crafted an Angular pipeline including build, tests, coverage and lint. This second part focuses on deployment: app docker image, GitLab pages and NPM packages.
You don't need advanced experience in GitLab to follow this article. This is a step by step guide with many links to the well-written documentation. If you use other CI/CD tools, you'll find similar concepts and commands.
Pipeline overviewLink to this section
During the first part of the series, you built jobs for install, build and tests jobs. Let's turn the basic pipeline we had into a complete pipeline with a deployment stage.
The pipeline suits both Angular applications and libraries, only the last part on publishing and deployment changes.
You'll learn how to deploy your app in two different ways. The first method is to publish a Docker image containing your app. With the second method, you'll host your app artifact on an HTTP server (GitLab Pages).
For the Angular library, the pipeline will publish it on the GitLab Package registry. The last part is a bonus to optimize the pipeline using custom docker images.
Build and publish Docker imageLink to this section
Releasing an Angular app means providing an artifact that's ready to deploy and run in production. The intuitive way is to provide the bundled app and let the DevOps folks upload it on a machine running a web server.
Instead, imagine you can provide a machine with your app ready to run. You have full control over the machine setup and can fine-tune the web server configuration as needed. This is where Docker comes in!
Quick Docker introLink to this section
Docker allows you to describe the machine setup with code. You can start from a clean Alpine or Ubuntu install and then run a set of script instructions to set up your application.
<>CopyFROM nginx:alpine COPY ./my-app /usr/share/nginx/html COPY ./my-app.conf /etc/nginx/conf.d/default.conf
The file that contains your instructions as code is a Dockerfile. After running this Dockerfile, you end up with a Docker image which is a snapshot of the machine with your app installed.


It's not possible to run a Docker image. You need to create a Docker container which uses the image as a blueprint. After starting this container, you have an isolated environment where your Angular app can run.
To learn more about the differences between Docker file, image and container, check out this article.
Design the GitLab jobLink to this section
This job purpose is to produce and make available a Docker image containing our Angular app. In a container-based infrastructure such as Kubernetes cluster, you need to provide a Docker image. The project artifact isn't the app bundle itself.


The pipeline will publish the image to the Container Registry. It enables softwares such as Docker or Kubernetes to pull and run it.
During a Kubernetes deployment, Kubernetes pulls the image from GitLab Container Registry before creating and running a Docker container out of it. The bundled app runs into this container with the runtime environment. Basically running the container starts the app.
Prepare a docker imageLink to this section
Do you remember the build_app job from the first article? Its purpose was to generate the bundled app and place it the artifacts/app
folder along with the Dockerfile and Nginx configuration.
<>Copyvariables: APP_OUTPUT_PATH: "$CI_PROJECT_DIR/artifacts/app" build_app: script: - yarn ng build --prod after_script: - cp $PROJECT_PATH/nginx.conf $APP_OUTPUT_PATH - cp $PROJECT_PATH/Dockerfile $APP_OUTPUT_PATH
Take a look at the bundled app, it's a single-page website (SPA) with a lot of scripts and a dash of styles.
You only need an HTTP server to host and serve your app. In our situation, we won't set up a server but rather create a docker image. Nginx looks like a reasonable choice for an HTTP server. Let’s use it in our Dockerfile.
<>CopyFROM nginx:alpine COPY . /usr/share/nginx/html COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY docker command moves the bundled app into the docker image. It replaces the default site but also the default configuration. The HTTP server will start with the container and expose port 80.
Angular apps are simple websites but we need to replace the default configuration. Try to access a sub route such as https://localhost:4200/test. You'll get a 404 page because Nginx is looking for a test folder that doesn't exist.
<>Copyserver { listen 80; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/index.html /index.html =404; } }
One solution is to provide a custom Nginx configuration. try_files
instruction redirects unknown paths to index.html
. In other words, it delegates routing to Angular router for unknown routes.
Create build and publish jobsLink to this section
As described in container registry documentation, it takes three steps:
- Log in into the project container registry
- Build the image from a Dockerfile
- Push the image to the project container registry
<>Copyvariables: DOCKER_IMAGE_NAME: "$CI_REGISTRY_IMAGE/app" publish_image: stage: publish tags: - shell before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - cd $APP_OUTPUT_PATH script: - docker build --tag $DOCKER_IMAGE_NAME:$CI_COMMIT_SHORT_SHA . - docker push $DOCKER_IMAGE_NAME:$CI_COMMIT_SHORT_SHA dependencies: - build_app only: - master
Earlier you used environment variables to get the project directory. There are plenty of variables but only the pipeline credentials are important here. The remaining steps are pure docker commands build & push.
Note the image got $CI_COMMIT_SHORT_SHA
tag. It would be convenient to use the latest
tag but using it is a bad practice. Each eligible pipeline will publish a docker image with a different version and tag.
You don't want to run this job for each pipeline, it runs only when pushing on master with the only keyword. Make sure to set up an expiration policy to clean the registry regularly. For the job to only download this artifact, we specify the dependency to the build_app job.


Deploy and run the imageLink to this section
A complete pipeline would trigger a deployment using the image. For instance, it can be a Kubernetes deployment pulling the image and spinning up containers.
This part is off the limit for this article but you can take a look at Kubernetes integration in GitLab. It enables a bunch of features such as Deployments, Logs, Monitoring and Review App in your project interface. GitLab supports some Kubernetes cluster solutions including Google Cloud Platform (GCP) and Amazon Elastic Kubernetes Service (EKS).
Let’s try to run the image on your local machine. It requires you to login to the container registry and run docker commands.
<>Copy$ docker login registry.gitlab.com $ docker pull registry.gitlab.com/jbardon/angular-app-pipeline/app:YOUR_TAG $ docker run --rm -i -p 4200:80 registry.gitlab.com/jbardon/angular-app-pipeline/app:YOUR_TAG
Check available tags in your container registry and replace YOUR_TAG
in the command below. For more information about the URL to use, read the official documentation.
Setup Docker-in-DockerLink to this section
Did you notice the tag for this job? It uses a shell executor instead of the usual docker executor. The reason is you can’t use docker to build a docker image inside a docker container out-of-the-box. The documentation describes several ways to build docker images in a pipeline:
- use a runner with shell executor having Docker up and running
- use docker:dind image which isn’t recommended and needs the privileged mode
- use Kaniko which don’t need the privileged mode
GitLab shared runners do have privileged mode enabled. You can try any of the three solutions, they’re all working at the time. Let’s focus on the shell executor solution as in an enterprise environment you’ll have your own runners you can install Docker on.
For testing purposes, you can host a runner on your local machine. GitLab provides Shared runners with free tier but not with shell executor and Docker installed.


To host a runner on your machine, you need to install gitlab-runner and register your local runner in your project. Once it’s done, the pipeline will run the job on your machine. Don’t forget it’s for testing only, your machine must be turned on with docker service started. Otherwise, the pipeline will be stuck waiting for a runner with the shell tag.
Deploy to GitLab PagesLink to this section
You learned how to build an Angular app docker image and make it available for deployment. Let's explore a simpler alternative: hosting the bundled app on an HTTP server. GitLab Pages allows hosting static websites for free.
Update build with subrouteLink to this section
Depending on your GitLab user, group and project names, you'll receive a default domain name. For instance, the example project from the first article of this series is named angular-app-pipeline
and its owner is jbardon
.
Example project GitLab Pages is accessible through:
https://jbardon.gitlab.io/angular-app-pipeline
The app isn't hosted on domain root but under /angular-app-pipeline
path. This detail is important because it won't work out-of-the-box. You need to provide extra options to ng-cli for the build.
<>Copy$ ng build --prod --base-href /angular-app-pipeline/ --deploy-url /angular-app-pipeline/
These options set the base href in index.html
. Also, all scripts and styles generated during the build will include the given path.
<>Copy<html lang="en"> <head> <base href="/angular-app-pipeline/"> <link rel="stylesheet" href="/angular-app-pipeline/styles.css"> </head> <body> <script src="/angular-app-pipeline/runtime.js" type="module"></script> </body> </html>
In the docker image job, we used a custom Nginx configuration to delegate the routing to Angular Router. It's not possible to do it with GitLab pages so we need to use a trick explained in Angular documentation.
The idea is to copy index.html
and name it 404.html
. I don't recommend it for production but it works since the HTTP server fallbacks on this page by default.
Job implementationLink to this section
Deploying on GitLab Pages is easy thanks to the pages keyword. The only rule is to output your website in the public
directory as an artifact.
<>Copyvariables: APP_OUTPUT_PATH: "$CI_PROJECT_DIR/artifacts/app" pages: stage: deploy tags: - shell script: - mv $APP_OUTPUT_PATH $CI_PROJECT_DIR/public artifacts: paths: - public dependencies: - build_app environment: name: prod url: https://jbardon.gitlab.io/angular-app-pipeline when: manual only: - master
Our Angular app is now hosted on GitLab Pages. Let's leverage Environments to follow the project deployments in several environments.
First, create a prod environment in the Operations/Environment page. You can now use the environment keyword in the job which deploys the project. In our example, there is only a prod environment for GitLab Pages.
If you enabled Kubernetes integration, the job may try to deploy on your cluster. Make sure no cluster has a prod environment in its scope.
Note using when:manual keyword is a good practice to avoid the job to deploy automatically. The pipeline stops when reaching the job and waits for a manual trigger. It gives the opportunity to watch the deployment job and checks everything works once it's done.
Deploy libraryLink to this section
Deploying an Angular app means serving it from an HTTP server or a Docker container running an HTTP server. This process is different for an Angular library because deploying a library means pushing it on a package registry. Don’t confuse it with the container registry reserved for Docker images.
It exists many public and private registries besides npmjs such as Artifactory and Nexus. Yet, GitLab offers a package registry with each project. If you need help to create an Angular library step by step, check out my article.
<>Copyvariables: LIBRARY_OUTPUT_PATH: "$CI_PROJECT_DIR/dist/angular-library" publish_library: stage: publish tags: - docker variables: REGISTRY_URI: "gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/npm/" before_script: - npm config set "@jbardon:registry" "https://$REGISTRY_URI" - npm config set "//$REGISTRY_URI:_authToken" "$CI_JOB_TOKEN" /projects/$CI_PROJECT_ID/packages/npm/" script: - cd $LIBRARY_OUTPUT_PATH - npm publish dependencies: - build_library only: - master
npm publish
is the command to publish a package. Yet, we need to override two configuration keys because we don't use the default registry (npmjs). These keys are set using the before_script keyword. They'll be taken into account by the job once running the main script.
NPM configurationLink to this section
The two configuration keys to update are:
- registry to target GitLab package registry
- authToken to be allowed to publish
Here is the extract from the job script. Both use the config set
command to set the value of a configuration key. Note the registry URI is slightly different for each config key.
<>Copy$ REGISTRY_URI="gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/npm/" $ npm config set "@jbardon:registry" "https://$REGISTRY_URI" $ npm config set "//$REGISTRY_URI:_authToken" "$CI_JOB_TOKEN"
There exists several ways to override the registry, let's see how it works with the config command. The example library full name is @jbardon/angular-lib-pipeline
with @jbardon
being the scope.
Your library must follow GitLab naming conventions. The package scope is your organisation or account name.
That's why the job overrides @jbardon:registry configuration key. It uses the project-level endpoint which allows publishing. Let's try another way to ensure manual publish targets GitLab project registry.
<>Copy{ "name": "@jbardon/angular-lib-pipeline", "version": "0.0.1", "publishConfig": { "@jbardon:registry": "https://gitlab.com/api/v4/projects/YOUR_PROJECT_ID/packages/npm/" }, "peerDependencies": { "@angular/common": "^10.1.0", "@angular/core": "^10.1.0" } }
Here the publishConfig
entry is equivalent to the configuration key. It's optional but adds the extra check for manual publishing. You can find the Project ID on the project main page under its title.
The second configuration, authToken
is exposed through the $CI_JOB_TOKEN
environment variable. If you use another package registry the two configurations are roughly the same. Be careful while adding your registry authToken. It’s not the API key but the token saved into ~/npmrc
when running npm login
.
Fetch your libraryLink to this section
Once the job is done, you can see the library in the project registry. The last published version is labeled with latest
, it’s the version you download by default. Don’t worry if you forgot to update the version in package.json
, it’s not possible to publish the same version twice.


While testing the job, I recommend using semver. You can publish pre-release versions such as 1.0.0-alpha.1
. It allows you to make several tests without bothering people pulling the registry and suggests the final version will be 1.0.0
.
Don’t be tempted to delete the last version and publish it again.
NPM won’t get the last version from the registry but install the last version from its local cache. The version is the same on both sides but the files are different.
<>Copy$ yarn config set @jbardon:registry https://gitlab.com/api/v4/packages/npm/ $ yarn login $ yarn add @jbardon/angular-lib-pipeline
In Package registry, click on the library. GitLab provides commands to install the library with NPM. You already know what the two first commands are for: set registry and authToken. Using the instance-level endpoint is enough for installing here. Plus, it’s the same for all the libraries you host on GitLab Package Registry.
Custom docker image for jobsLink to this section
Most of the job runs on the docker executor using the image keyword. For the install_dependency job, the whole node environment is set up.
In some cases, like during the test_app job, it's necessary to use the before_script keyword to perform some extra setup before the job runs. The extra setup can take a while, for instance installing Chrome for unit tests can take up to 30 seconds each time it's running.
<>Copytest_app: image: node:12-alpine tags: - docker before_script: - apk add chromium - export CHROME_BIN=/usr/bin/chromium-browser
Docker can help to solve this issue by creating a new image based on node:12-alpine
which includes what the before_script does. The job will run this new image containing the required tools so it doesn't need to install them.
<>CopyFROM node:12_alpine RUN apk add chromium ENV CHROME_BIN /usr/bin/chromium-browser
By default, the runner pulls docker images from the docker.io registry. Remember GitLab provides a Container Registry with each project? Let's leverage this feature and push our image to this private registry instead.
<>Copy$ docker build --tag=ci-tests:latest . $ docker login registry.gitlab.com $ docker push registry.gitlab.com/jbardon/angular-app-pipeline/ci-tests:latest
The last step is to make sure the job pulls the image from the project registry. You only have to append the image name with the corresponding environment variable.
image: $CI_REGISTRY_IMAGE/ci-node:latest
Now the pipeline goes fast and doesn't lose time with environment setup. Yet, updating images used by CI and pushing them into the project registry isn't a robust workflow.
Automate custom image updateLink to this section
You can take this step forward and make the pipeline build and push the images itself when needed.
You already built and published a Docker image to the project container registry before. In this context, the job script is the same: login, build and push. Note the parallel:matrix keyword, which enables you to run the whole job multiple times with parameters.
<>Copyupdate_ci_images: stage: .pre tags: - shell before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - cd $PROJECT_PATH/.ci script: - docker build --tag $CI_REGISTRY_IMAGE/$STAGE_IMAGE:latest --target $STAGE_IMAGE $PROJECT_PATH/.ci - docker push $CI_REGISTRY_IMAGE/$STAGE_IMAGE:latest parallel: matrix: - STAGE_IMAGE: [ci-node, ci-tests] only: changes: - .ci/Dockerfile
The idea is to have the Dockerfile for each image used by the pipeline in the repository. All images are defined in a single Dockerfile under .ci/Dockerfile
. It's placed into an empty directory so each image has its context.
Having the Dockerfile in the repository allows triggering the job for building the images only when it changes with only:changes keyword. This building images job always runs first in the pipeline thanks to the .pre keyword.
<>CopyFROM node:12-alpine AS ci-node FROM ci-node AS ci-tests RUN apk add chromium ENV CHROME_BIN /usr/bin/chromium-browser
This example leverages multi-stage builds so a single file can define many images. Depending on the specified target when running docker build
, two images can be created from this Dockerfile: ci-node and ci-tests.
Wrapping upLink to this section
That's it, you learned how to build a complete GitLab pipeline including deployment for Angular apps and Angular libraries.
With this pipeline, you can deploy your Angular app on static sites free hosting platforms such as GitLab pages. For a more production-ready approach prefer to go for the docker image method. We also leveraged GitLab using both Container and package registries. It hosts the Angular app and pipeline docker image but also our Angular library.
Here are two GitLab projects using this pipeline
- https://gitlab.com/jbardon/angular-app-pipeline
- https://gitlab.com/jbardon/angular-lib-pipeline
A few last pieces of advice to develop your pipeline. Use CI Lint tool to debug it and read this documentation about Pipeline efficiency. I'm sure you'll find improvement for this pipeline, don't hesitate to drop a comment.
If you liked this article or if you are curious about how we innovate at Smart AdServer, take a look at our official Smart AdServer blog. See you there!
Thanks to the reviewers who helped me to make this article better : Gaurav Dasgupta and Max Koretskyi from InDepthDev community.
Comments (0)
Be the first to leave a comment
About the author

Avid learner about web development and writer for sharing tips and tricks, Full-Stack Developer @SmartAdserver

About the author
Jérémy Bardon
Avid learner about web development and writer for sharing tips and tricks, Full-Stack Developer @SmartAdserver
About the author

Avid learner about web development and writer for sharing tips and tricks, Full-Stack Developer @SmartAdserver