It is very easy to turn an application into a container image that you can publish, share and deploy using any container runtime, cloud service or Kubernetes (lookalike). The tool you can use for this is called Buildpacks. And the steps you take are very simple.
In this article I show a straightforward sequence of steps for creating an Angular web application with NodeJS backend. With a single command I produce the container image that contains the application, the dependencies and required runtimes. A container can be started from this image and runs the application.
The article illustrates a Gitpod workspace in which a fresh Angular application is created with a NodeJS backend. Buildpack is installed in the workspace and is used to produce a Container Image for the application. Subsequently, a container is started from this image to demonstrate it works as expected.
The steps in this article are as follows:
- create a fresh GitHub repository
- open a Gitpod workspace for this repository
- install the Angular CLI and create a fresh Angular web application (test build & run the application)
- add a NodeJS application as the backend — to serve static resources for the Angular application and handle HTTP requests from the web application (currently none are implemented) (test run the web application through the NodeJS server)
- install the Pack CLI- the Buildpacks client
- generate the container image for the Angular application using Buildpacks
- run a container based on the container image and demonstrate that the Angular application is running
- update the application, reproduce the container image, run a fresh container
Executing the steps
Note: for the purpose of trying out Buildpacks, I could create any type of application (Java/SpringBoot, Kotlin, Python/Flask, C#/Blazor, ..). As I happened to explore Angular at this time, that is what I arrived at. I could have created the application anywhere — on my laptop, in a Codespace but I happen to work a lot in Gitpod workspaces, so that is the context for this article.
Step 1: create a new repository in GitHub: https://github.com/lucasjellema/my-angular-app-in-container
Create a new empty repository in GitHub
Step 2: open a Gitpod workspace for this repository. Add the prefix “https:\\gitpod.io\#” to the GitHub Repository URL: https://gitpod.io/#https://github.com/lucasjellema/my-angular-app-in-container
Open a fresh Gitpod workspace for the GitHub repository
Click on Continue to launch and enter the workspace:
Click on Continue to launch the workspace
The Gitpod workspace opens in my browser. It presents VS Code in the browser and it runs a virtual machine with various runtimes — such as Java, Python and Node — and tooling such as Docker, Docker Compose, git, npm, pip. maven and more.
The VS Code in the browser that presents the Gitpod workspace
Step 3: Install the Angular CLI
npm install -g @angular/cli
Install the Angular CLI
Step 4: Create a new Angular application
ng new my-angular-app
Creation of a brand new Angular application
The application at this point is not very interesting — but it is interesting to make sure that it runs, using:
cd my-angular-app
ng server
Run the generated Angular application
This command will start the built in web server and make the application accessible through port 4200. We can launch a new browser window at that port:
Inspect the Angular application , exposed at port 4200 (Gitpod create a custom URL that starts with the port-number where our local browser can access the remotely running Angular application)
Step 5: Add NodeJS backend to serve the application (and later on handle API calls from the web application)
NodeJS as runtime is already available. In order to add a NodeJS backend that acts as webserver, we need a few things. Add a file server.js
in the root of the application:
const express = require('express');
const path = require('path');
const app = express();
// Serve static files from the "dist" directory
app.use(express.static(path.join(__dirname, 'dist/my-angular-app/browser')));
// Route to serve files based on the request
app.get('/:filename', (req, res) => {
const filename = req.params.filename;
const filePath = path.join(__dirname, 'dist/my-angular-app/browser', filename);
res.sendFile(filePath, (err) => {
if (err) {
console.error(`File not found: ${filename}`);
res.status(404).send('File not found');
}
});
});
// Fallback to index.html for Angular routing (if using Angular's Router)
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist/my-angular-app/browser/index.html'));
});
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Add the dependency for express to package.json
and also change the start script to "start": "node server.js"
{
"name": "my-angular-app",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "node server.js",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"express": "^4.17.1",
...
Make sure the dependency is installed:
npm install
Now, run the application using
npm run start
This kicks off the Node application that starts listening for requests on port 8080.
Run the Node application that starts listening at port 8080
If we open a browser window a browser window at that port, a request is sent to server.js and the result is that the Angular application is served to the browser from this Node application.
Again the Angular application, this time served from the Node application (from port 8080 instead of port 4200 that ng serve was listening on)
Gitpod again creates a custom endpoint that starts with 8080, the port at which the Node application is listening.
The current set up in the Gitpod workspace with the Node application serving the Angular web application
Step 6: Install the Buildpacks CLI Pack
The Pack CLI for Buildpacks is installed using:
sudo add-apt-repository ppa:cncf-buildpacks/pack-cli
sudo apt-get update
sudo apt-get install pack-cli
Installing the Pack CLI for Buildpacks
Step 7: Make Buildpacks produce the Container Image for the Angular application
And now the moment you have been waiting for. I have this Angular +NodeJS application. It is easy enough to run it locally. Just kickoff the NodeJS runtime that was pre installed along with the node modules and other dependencies and the application runs. But how to turn it into a stand alone container image that contains all it needs (and not more) and can be shipped and deployed anywhere (containers can run). This is what Buildpacks does for us. It recognizes the technology used in the application, it knows which runtimes to add in the container image and how to build and package the application and how to expose it and run it when the container is started from the image.
I first need to allow access to the Docker daemon (a special step in the Gitpod workspace I believe)
chmod a+w /var/run/docker.sock
and then I can start the generation of the container:
pack build my-angular-app --builder gcr.io/buildpacks/builder:v1
The process that takes place now is visualized in this picture:
The Builder is retrieved, the source code is inspected and identified and the appropriate container image build steps are executed. The end result is a container image with the built application and the required runtime engines and dependencies
Here is a little of the output from this build process:
Starting the container image build process
And here is the final piece of the build process. It takes between 30–90 seconds.
Final logging from the build process
Step 8: Run a container from Angular / Node Application Container Image
As the container image is complete, we can save it to a repositorym, share and ship it and run it. We no longer have to concern ourselves with the contents of the container image. We know it is Node and Angular but to work with it, deploy it and run it, we do not need to know nor care.
A container can be started in the conventional way:
docker run -p 8081:8080 my-angular-app
Docker run followed by the name of the container image — untagged, so from the local registry. And with a port mapping: the port 8080 inside the container is mapped to port 8081 on the outside (because port 8080 on our Ubuntu host is still used by the original Node application)
Run a container from the freshly built container image — listening at port 8081
When we open the browser, we get to see the application (of course), this time served from the browser, at port 8081:
The Angular application, served by the Node application from the Container
The situation we now see is as shown in this picture:
The application is running twice: once directly on Ubuntu using the Node runtime listening at port 8080 and a second time in the container, listening at port 8081
The application is running twice: once directly on Ubuntu using the Node runtime listening at port 8080 and a second time in the container, listening at port 8081 (mapped internally to port 8080). This second instance can easily be deployed on any container runtime anywhere in the world: it does not have any external dependencies.
Conclusion
Buildpack is very convenient — especially to quickly get an application containerized (where perhaps you do not need a lot of finetuning of the resulting container image and are mainly interested in getting the application running in a container as quickly as possible). For trying out technologies, quickly sharing prototypes and for plain fun, I like it a lot.