Your Angular apps as Docker containers
In this article, I am going to show you how to create a Docker image from your Angular application, publish and consume it from the Docker Hub.
In this article, I am going to show you how to create a Docker image from your Angular application, publish and consume it from the Docker Hub.
Creating a Docker image from your Angular application is easy.
You can use images for internal purposes, for example, testing on different machines, operating systems, and browsers.
You can also use Docker images for external use.
It becomes possible to try your application without installing node.js,
running npm install
to “download entire internet,” or avoid troubleshooting environment setup.
Before we start, let’s generate a new application using the following Angular CLI command:
ng new my-angular-app
cd my-angular-app
Try running the application to ensure it compiles correctly and displays in the browser:
ng serve --open
By default, the browser should open with the “http://localhost:4200” address. Note the port 4200; later in the article, we are going to use also port 3000 to access the application that is running from the Docker container and not the local folder.
Now as we got the base application up and running, let’s create a new Dockerfile to build an image.
We are going to use Nginx to serve the compiled application.
Let’s take the stable alpine
version to get the smallest output:
FROM nginx:stable-alpine
LABEL version="1.0"
COPY nginx.conf /etc/nginx/nginx.conf
WORKDIR /usr/share/nginx/html
COPY dist/my-angular-app/ .
As you can see from the example above, we are going to copy the dist
folder inside the image,
together with the nginx.conf
configuration file.
A straightforward configuration implementation can look like in the next example:
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
include /etc/nginx/mime.types;
gzip on;
gzip_min_length 1000;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
location / {
try_files $uri $uri/ /index.html;
}
}
}
Next, we need to get the dist
folder, containing the compiled application.
It can be either a development or production build. Let’s create the optimized production one:
ng build --prod
Never try to run npm install or yarn install inside the production image on startup. When it comes to scaling and creating multiple containers, the last thing you want is hundreds of images downloading lots of data at startup. You should always aim to serve the final production builds inside your Docker images.
Now, let’s build the image and call it my-angular-app:
docker image build -t my-angular-app .
The Docker should produce an output similar to the following one but with different hash codes:
Sending build context to Docker daemon 346.5MB
Step 1/5 : FROM nginx:stable-alpine
---> 14d4a58e0d2e
Step 2/5 : LABEL version="1.0"
---> Running in c44ea1c70237
Removing intermediate container c44ea1c70237
---> 4be87ff53fdb
Step 3/5 : COPY nginx.conf /etc/nginx/nginx.conf
---> 2f77c43d1ee5
Step 4/5 : WORKDIR /usr/share/nginx/html
---> Running in bacd618bf2d9
Removing intermediate container bacd618bf2d9
---> fd79cc68b583
Step 5/5 : COPY dist/ .
---> 1ce2602f089f
Successfully built 1ce2602f089f
Successfully tagged my-angular-app:latest
The process should be relatively fast. Once it is complete, feel free to test the image is there, by running the next command:
docker image ls
You should see a list of available images, including the my-angular-app
one with the tag latest
and about 18 MB size.
It is now time to test the image. Run the command below to create a container and map it to the port 3000:
docker run -p 3000:80 --rm my-angular-app
If you now navigate to the http://localhost:3000, you should get your initial Angular application up and running there. Moreover, the Nginx logs are redirected to your console output, so that you can see what’s happening inside the container:
Take a closer look at the console output when building the image. You are sending more than 300 MB each time as a build context for the Docker daemon:
Sending build context to Docker daemon 346.5MB
It is sending the whole project folder content,
while all you need is the dist
folder that contains the build output.
Note, however, that the data does not end up in the resulting Docker image,
but it significantly slows down the entire process.
You can improve the process by adding a .dockerignore
file with the files and directories
to exclude from the Docker build:
e2e
node_modules
src
Run the build command once again:
docker image build -t my-angular-app .
This time, the output should be as following:
Sending build context to Docker daemon 1.071MB
That is an excellent result. The build should be almost instant, and the size of the context gets reduced from 345 MB to 1 MB.
That is all you need to build an image on your local machine! Let’s now proceed to provide support for multi-container Docker setup.
At some point, you will most probably end up with multiple containers each serving a particular need, for example, a separate backend server, a database server and so forth.
Here’s a simple docker-compose.yml
file you can prepare as a template for your Angular application:
version: '3.1'
services:
app:
image: 'my-angular-app'
build: '.'
ports:
- 3000:80
Note the build
entry provided for development purposes.
That means we are always going to build the local image if it is not available.
You can remove this line later on once you start publishing images to Docker Hub.
Now use the next command to build the image and run it in a container:
docker-compose up
As before, navigate to http://localhost:3000 to ensure the application runs as expected.
Once you are done playing with the application, you can clean up everything using the next command:
docker-compose down
If you want to remove everything, including the image created earlier, use the following command:
docker-compose down --rmi all
Next time when you run the docker-compose up
command, you may see the following message:
WARNING: Image for service app was built because it did not already exist.
To rebuild this image you must use `docker-compose build`
or `docker-compose up --build`.
That is a proof that Docker tool automatically builds a new image if it is not available. Now, let’s try to publish our image to Docker Hub so that other users can use it.
You should have a Docker Hub account to publish images. The account is free, and you can create it here: https://hub.docker.com/.
Once you have an account, run the login command in the terminal app and fill the username and password to be able publishing images:
docker login
Publishing your image is easy. Just run the following command in the root folder of your project,
and replace the account
value with your Docker Hub username.
docker image build -t account/my-angular-app:1.0 .
That creates a new image, ready to be deployed for your account.
Use the next command to publish it, and don’t forget to use your Docker Hub username instead of the account
value.
docker push account/my-angular-app:1.0
The process should be high-speed, and in a few seconds, your image is going to be public and available for use.
Now, as you project is published, you or any other person can run it locally by utilizing the following command:
docker run -p 3000:80 account/my-angular-app:1.0
To quickly test the container and automatically clean everything up once it got stopped, you can add the --rm
switch:
docker run -p 3000:80 --rm account/my-angular-app:1.0
Finally, to use the docker-compose.yml
file with your published image,
just remove the build
option and point the image value to the published version:
version: '3.1'
services:
app:
image: 'account/my-angular-app'
ports:
- 3000:80
You can also dockerize the application build process by utilizing the “staged builds” feature. The flow can be as follows:
The primary benefit of such an approach is that you don’t have to install NPM and other tooling dependencies on your local machine or CI/CD server. Also, that raises a security level for your build process as third-party libraries with custom “postinstall” scripts won’t be able accessing the data outside the image boundaries.
First, you need to pass the source code to the Docker context,
so remove the src
rule from the .dockeringnore
file and leave it like the following:
e2e
node_modules
Now, update the Dockerfile
according to the next listing:
# Step 1: Build the app in image 'builder'
FROM node:12.8-alpine AS builder
WORKDIR /usr/src/app
COPY . .
RUN yarn && yarn build
# Step 2: Use build output from 'builder'
FROM nginx:stable-alpine
LABEL version="1.0"
COPY nginx.conf /etc/nginx/nginx.conf
WORKDIR /usr/share/nginx/html
COPY --from=builder /usr/src/app/dist/my-angular-app/ .
Run the build command once again:
docker image build -t my-angular-app .
At this point, you are now able to setup dependencies and build the Angular application without having NPM or Angular CLI installed. Typically, you may want to have such setup for CI/CD integration too.
To test the flow, update the application title but don’t build the application locally.
For the sake of simplicity, edit the app.component.html
file
and replace the <h1>
element with the following markup:
<h1>
Welcome to Dockerized {{ title }}!
</h1>
Rebuild and start the application using the following commands:
docker image build -t my-angular-app .
docker run -p 3000:80 --rm my-angular-app
Check the ”http://localhost:3000”, and you should see the updated application up and running:
To reduce the time for typing the commands, you can put the “build” and “start” commands to the “package.json” file:
{
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --prod",
"build:docker": "docker image build -t my-angular-app .",
"start:docker": "docker run -p 3000:80 --rm my-angular-app",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
}
}
Now, every time you need to build a new image, run npm run build:docker
.
To test the application from within the container, use npm run start:docker
command.
When preparing the production builds, the Angular CLI always addresses the browser caching by appending the hash sums to the file name.
This technique is often called “cache busting” and helps browsers to re-download the files only if the content has changed.
The hash code in the name is directly related to the content of the file.
As soon as you make a modification to the source code, the resulting hash changes and Angular CLI updates the index.html
file.
That instructs the user browser to download a fresh copy of the changed files only.
As an example, run the “npm run build” command and check the dist/app/index.html
contents.
It should end up with the set of scripts similar to the following listing:
<body>
<app-root></app-root>
<script type="text/javascript" src="runtime.26209474bfa8dc87a77c.js"></script>
<script
type="text/javascript"
src="es2015-polyfills.c5dd28b362270c767b34.js"
nomodule
></script>
<script
type="text/javascript"
src="polyfills.dfd9644fffcf7f355e2e.js"
></script>
<script type="text/javascript" src="main.665a5dcb77766956827f.js"></script>
</body>
The most common problem that occurs with such approach is that index.html
does not change
and users may always get the cached version, while the JavaScript files may get replaced with the new builds and deployments.
In this case, users get a blank page as soon as a new version of your Angular application gets deployed.
The solution I suggest for the Docker image is to bust the cache for the index.html
file for all page requests.
You can do that in the nginx.conf
configuration file:
location ~ \.html$ {
add_header Cache-Control "private, no-cache, no-store, must-revalidate";
add_header Expires "Sat, 01 Jan 2000 00:00:00 GMT";
add_header Pragma no-cache;
}
With Angular, you usually get a single-page web application with a single HTML file pointing to JavaScript bundles. So busting the cache for this file won’t hurt the performance, but ensures your users are always using the correct versions of the compiled bundles.
Running your Angular applications with Docker containers can significantly reduce your development and testing efforts, especially if you intend frequently sharing your application either internally or with external users.
One of the most excellent benefits is that people who test or use your application do not need to use development tools to compile your project from the source code or configure a web server to run the compiled output. Everything comes with a container and runs on all operating systems without extra overhead.
You can find the source code for the article in the following GitHub repository: https://github.com/DenysVuika/medium-angular-docker.