In this tutorial, we'll containerise a simple Express app into a rock
using Rockcraft's expressjs-framework :ref:`extension
<reference-express-framework>`.
It should take 25 minutes for you to complete.
You won’t need to come prepared with intricate knowledge of software packaging, but familiarity with Linux paradigms, terminal operations, and Express is required.
Once you complete this tutorial, you’ll have a working rock for an Express
app. You’ll gain familiarity with Rockcraft and the
expressjs-framework extension, and have the experience to create
rocks for Express apps.
In order to test the Express app locally, before packing it into a rock, install npm and initialize the starter app.
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:install-deps]
:end-before: [docs:install-deps-end]
:dedent: 2
Start by creating the "Hello, world" Express app that we'll pack in this tutorial.
Create an empty project directory:
mkdir expressjs-hello-world
cd expressjs-hello-worldNext, create a skeleton for the project with the Express app generator:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:init-app]
:end-before: [docs:init-app-end]
:dedent: 2
Note
During the npm install step, npm performs a security scan of downloaded dependencies, and it warns about any known vulnerabilities in installed packages.
This warning is expected for this tutorial and should not affect the remaining steps. For production use, review and address dependency vulnerabilities before deployment.
Let's run the Express app to verify that it works:
npm startThe app starts an HTTP server listening on port 3000
that we can test by using curl to send a request to the root
endpoint. We'll need a new shell of the VM for this -- in a separate terminal,
run multipass shell rock-dev again:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:curl-expressjs]
:end-before: [docs:curl-expressjs-end]
:dedent: 2
The Express app should respond with Welcome to Express web page.
Note
The response from the Express app includes HTML and CSS which
makes it difficult to read on a terminal. Visit
http://<Multipass private IP>:3000 using a browser to see the fully
rendered page, replacing <Multipass private IP> with your VM's
private IP address. To determine the IP address of your VM, outside of the
VM, run:
multipass info rock-dev | grep IPThe Express app looks good, so let's close the terminal instance we used for testing and stop the app in the original terminal instance by pressing Ctrl + C.
Now let's create a container image for our Express app. We'll use a rock, which is an OCI-compliant container image based on Ubuntu.
First, we'll need a rockcraft.yaml project file. We'll take advantage of a
pre-defined extension in Rockcraft with the --profile flag that caters
initial rock files for specific web app frameworks. Using the
Express profile, Rockcraft automates the creation of
rockcraft.yaml and tailors the file for an Express app.
From the ~/expressjs-hello-world directory, initialize the rock:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:create-rockcraft-yaml]
:end-before: [docs:create-rockcraft-yaml-end]
:dedent: 2
The rockcraft.yaml file will automatically be created and set the name
based on your working directory.
Check out the contents of rockcraft.yaml:
cat rockcraft.yamlThe top of the file should look similar to the following snippet:
name: expressjs-hello-world
# see https://documentation.ubuntu.com/rockcraft/latest/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: bare # as an alternative, an ubuntu base can be used
build-base: [email protected] # build-base is required when the base is bare
version: '0.1' # just for humans. Semantic versioning is recommended
summary: A summary of your ExpressJS app # 79 char long summary
description: |
This is expressjs-hello-world's description. You have a paragraph or two to tell the
most important story about it. Keep it under 100 words though,
we live in tweetspace and your description wants to look good in the
container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
amd64:
# arm64:
# ppc64el:
# s390x:
...Verify that the name is expressjs-hello-world.
Ensure that platforms includes the architecture of your host. Check
the architecture of your system:
dpkg --print-architectureEdit the platforms key in rockcraft.yaml if required.
Pack the rock:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:pack]
:end-before: [docs:pack-end]
:dedent: 2
Once Rockcraft has finished packing the Express rock, we'll find a new file in
the working directory (an `OCI <OCI_image_spec_>`_ image) with the .rock
extension:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:ls-rock]
:end-before: [docs:ls-rock-end]
:dedent: 2
We already have the rock as an OCI image. Now we need to load it into Docker. Docker requires rocks to be imported into the daemon since they can't be run directly like an executable.
Copy the rock:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:skopeo-copy]
:end-before: [docs:skopeo-copy-end]
:dedent: 2
This command contains the following pieces:
--insecure-policy: adopts a permissive policy that removes the need for a dedicated policy file.oci-archive: specifies the rock we created for our Express app.docker-daemon: specifies the name of the image in the Docker registry.
Check that the image was successfully loaded into Docker:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:docker-images]
:end-before: [docs:docker-images-end]
:dedent: 2
The output should list the Express image, along with its tag, ID and size:
.. terminal::
:user: ubuntu
:host: rock-dev
:dir: ~/expressjs-hello-world
sudo docker images expressjs-hello-world:0.1
REPOSITORY TAG IMAGE ID CREATED SIZE
expressjs-hello-world 0.1 30c7e5aed202 2 weeks ago 304MB
Now we're finally ready to run the rock and test the containerised Express app:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:docker-run]
:end-before: [docs:docker-run-end]
:dedent: 2
Use the same curl command as before to send a request to the Express app's root endpoint which is running inside the container:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:curl-expressjs-rock]
:end-before: [docs:curl-expressjs-rock-end]
:dedent: 2
The Express app again responds with Welcome to Express page.
When deploying the Express rock, we can always get the app logs with :ref:`explanation-pebble`:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:get-logs]
:end-before: [docs:get-logs-end]
:dedent: 2
As a result, Pebble will give us the logs for the
expressjs service running inside the container.
We should expect to see something similar to this:
.. terminal::
:user: ubuntu
:host: rock-dev
:dir: ~/expressjs-hello-world
sudo docker exec expressjs-hello-world pebble logs expressjs
[email protected] start
node ./bin/www
GET / 200 62.934 ms - 170
We can also choose to follow the logs by using the -f option with the
pebble logs command above. To stop following the logs, press Ctrl +
C.
Now we have a fully functional rock for a Express app! This concludes the first part of this tutorial, so we'll stop the container and remove the respective image for now:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:stop-docker]
:end-before: [docs:stop-docker-end]
:dedent: 2
For our final task, let's update our app. As an example,
let's add a new /time endpoint that returns the current time.
Start by creating the app/routes/time.js file in a text editor and paste the
code from the snippet below:
.. literalinclude:: code/expressjs/time.js
:caption: ~/expressjs-hello-world/app/routes/time.js
:language: javascript
Place the code snippet below in app/app.js under routes registration section
along with other app.use(...) lines.
It will register the new /time endpoint:
.. literalinclude:: code/expressjs/time_app.js
:caption: ~/expressjs-hello-world/app/app.js
:language: javascript
:start-after: [docs:append-lines]
:end-before: [docs:append-lines-end]
Since we are creating a new version of the app, set
version: '0.2' in the project file.
The top of the rockcraft.yaml file should look similar to the following:
name: expressjs-hello-world
# see https://documentation.ubuntu.com/rockcraft/latest/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: bare # as an alternative, an ubuntu base can be used
build-base: [email protected] # build-base is required when the base is bare
version: '0.2'
summary: A summary of your ExpressJS app # 79 char long summary
description: |
This is expressjs-hello-world's description. You have a paragraph or two to tell the
most important story about it. Keep it under 100 words though,
we live in tweetspace and your description wants to look good in the
container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
amd64:
# arm64:
# ppc64el:
# s390x:Note
If we repack the rock without changing the version, the new rock will have the same name and overwrite the last one we built. It's a good practice to change the version whenever we make changes to the app in the image.
Pack and run the rock using similar commands as before:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:docker-run-update]
:end-before: [docs:docker-run-update-end]
:dedent: 2
The resulting .rock file will be named differently, as
its new version will be part of the filename.
Finally, use curl to send a request to the /time endpoint:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:curl-time]
:end-before: [docs:curl-time-end]
:dedent: 2
The updated app should respond with the current date and time (e.g.
Fri Jan 10 2025 03:11:44 GMT+0000 (Coordinated Universal Time)).
Tip
If you are getting a 404 for the /time endpoint, check the
:ref:`troubleshooting-expressjs` steps below.
We can now stop the container and remove the corresponding image:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:stop-docker-updated]
:end-before: [docs:stop-docker-updated-end]
:dedent: 2
We've reached the end of this tutorial.
If we'd like to reset the working environment, we can simply run the following:
.. literalinclude:: code/expressjs/task.yaml
:language: bash
:start-after: [docs:cleanup]
:end-before: [docs:cleanup-end]
:dedent: 2
We can also clean the Multipass instance up. Start by exiting it:
exitAnd then we can proceed with its deletion:
multipass delete rock-dev
multipass purgeCongratulations! You've reached the end of this tutorial. You created a Express app, packaged it into a rock, and practiced some typical development skills such as viewing logs and updating the app.
But there is a lot more to explore:
| If you are wondering... | Visit... |
|---|---|
| "What's next?" | :external+charmcraft:ref:`Write your first Kubernetes charm for an Express app in Charmcraft <write-your-first-kubernetes-charm-for-a-expressjs-app>` |
| "How do I...?" | :ref:`how-to-manage-a-12-factor-app-rock` |
| "How do I get in touch?" | Matrix channel |
| "What is...?" | :ref:`expressjs-framework extension <reference-express-framework>` |
| "Why...?", "So what?" | :external+12-factor:ref:`12-Factor app principles and support in Charmcraft and Rockcraft <explanation>` |
App updates not taking effect?
Upon changing the Express app and re-packing the rock, if
the changes are not taking effect, try running rockcraft clean and pack
the rock again with rockcraft pack.