Red Hat Developer image

In my previous article, Run Red Hat Enterprise Linux 8 in a container on RHEL 7, I showed how to start developing with the latest versions of languages, databases, and web servers available with Red Hat Enterprise Linux 8, even if you are still running RHEL 7. In this article, I’ll build on that base to show how to get started with Node using the current RHEL 8 application stream versions of Node.js and Redis 5.

From my perspective, using Red Hat Enterprise Linux 8 application streams in containers is preferable to using software collections on RHEL 7. While you need to get comfortable with containers, all of the software installs in the locations you’d expect. There is no need to use scl commands to manage the selected software versions. Instead, each container gets an isolated user space. You don’t have to worry about conflicting versions.

In this article, you’ll create a Red Hat Enterprise Linux 8 Node.js container with Buildah, and run it with Podman. The code will be stored on your local machine and mapped into the RHEL 8 Node.js container when it runs. You’ll be able to edit the code on your local machine as you would any other application. Because it is mapped via a volume mount, the changes you make to the code will be immediately visible from the container, which is convenient for dynamic languages that don’t need to be compiled. This method isn’t the way you’d want to do things for production, but it gets you started developing quickly and should give you essentially the same development inner loop as you’d have when developing locally without containers. This article also shows how you can use Buildah to build an image with your completed application that you could use for production.

Additionally, you’ll set up the Red Hat Enterprise Linux 8 Redis application stream in a container that is managed by systemd. You’ll be able to use systemctl to start and stop the container just as you would for a non-container installation.

Installing Podman and Buildah on Red Hat Enterprise Linux 7

First, we need to install Podman, which is in the extras repo on Red Hat Enterprise Linux 7. The extras repo isn’t enabled by default. It is recommended that developers also enable the rhscl (Red Hat Software Collections), devtools, and optional repos:

$ sudo subscription-manager repos --enable rhel-7-server-extras-rpms \
    --enable rhel-7-server-optional-rpms \
    --enable rhel-server-rhscl-7-rpms \
    --enable rhel-7-server-devtools-rpms

Now install Podman and Buildah. If sudo isn’t set up on your system, see How to enable sudo on Red Hat Enterprise Linux.

$ sudo yum install podman buildah

Later, we’ll run containers with systemd. If SELinux is enabled on your system (it is by default), you must turn on the container_manage_cgroup boolean to run containers with systemd. For more information, see the container running systemd solution.
Note: The Red Hat ID that was created when you joined Red Hat Developers gives you access to content on the Red Hat Customer Portal.

$ sudo setsebool -P container_manage_cgroup on 

Running Node.js in a Red Hat Enterprise Linux 8 UBI container

We’ll need Node.js in a container we can use for development. We could pull down the Red Hat Enterprise Linux 8 Universal Base Image (UBI) and then yum install nodejs to create our own base image, but fortunately, Red Hat has already done that and it’s free to use and redistribute. For more information about UBI, see the section "Red Hat Universal Base Images" below.

Red Hat has a new container registry that uses authentication: registry.redhat.io. A Red Hat account isn’t required to use UBI images. However, other Red Hat images that aren’t part of UBI can only be obtained through this registry. The Red Hat ID that was created when you joined Red Hat Developers gives you access to the Red Hat Container Registry, so for simplicity, I use only registry.redhat.io. If you aren’t logged in when you try to pull an image, you’ll get a verbose error message. If you look carefully you’ll see:

...unable to retrieve auth token: invalid username/password.

Log in with your Red Hat username and password:

$ sudo podman login registry.redhat.io

Note: Podman has been designed so that it can be run without root. However, the support for it isn’t there with Red Hat Enterprise Linux 7.6. For more information, see Scott McCarty’s A preview of running containers without root in RHEL 7.6.

To see what Node.js container images are available, you could search the Red Hat Container Catalog, or you could search using the command-line interface (CLI):

$ sudo podman search registry.redhat.io/ubi8

When this article was written, the current application stream version was nodejs-10. Pull the image down to your local system:

$ sudo podman pull registry.redhat.io/ubi8/nodejs-10

Setting up a Node.js example app

We have a container with Node.js installed, but we need code to run. We’ll use React.js to create the code for a quick "Hello, World" that will run in the container but can be accessed from a browser on the host system.

To make things easy for development, we won't copy the code into the container. Instead, we’ll set things up to map a source directory from the host system into the container.

Since we are working with files that are shared between your host system and the container, we’ll make the container run using the same numeric user ID (UID) as your account on the host system. If something running inside the container creates files in the source directory, they will be owned by your user ID. Find out your UID and GID with the id command:

$ id

Make a note of the number after UID= and GID= at the very start of the line. On my system, my UID and GID are both 1000, so you will see these values reflected in the Podman and Buildah commands in this article. Change those values to match your UID and GID.

Run the following commands to create a source directory in a convenient location to share with the container:

$ sudo mkdir -p /opt/src/
$ sudo chown -R $USER:$USER /opt/src

Creating the React app in the container

We’ll use an npx command to create the example app. The current versions of node, npm, and npx are installed in the container so we’ll need to do our work inside the container. To do this, start a container running the nodejs image:

$ sudo podman run --rm -it --user 1000:1000 -v /opt/src:/opt/app-root/src:Z --net host registry.redhat.io/ubi8/nodejs-10 /bin/bash

Let’s look a what the above command did. It:

  • Arranged for the container to be deleted when it exits.
  • Set the container to interactive, running in the foreground.
  • Set the process(es) in the container to run as UID 1000 and GID 1000.
  • Mapped the host system’s /opt/src directory into the container as /opt/app-root/src to give the container access to our source directory.
  • Set the container to share the host’s network. (This action makes any ports used by the app in the container accessible from the host system.)
  • Ran an interactive bash shell in the container.

Now, run these commands using the bash shell inside the container:

$ npx create-react-app react-web-app
$ cd react-web-app
$ npm start

At this point, the newly created React app is running inside the container. Using a browser on your host system, go to http://localhost:3000/. You will see:

Leave the container running. On the host system, go to /opt/src/react-web-app/src.  Then, use an editor to edit App.js. When you save the file, the Node.js app running in the container will notice and tell the browser running on your host system to reload the page. You should now be able to develop your application in mostly the same way you would if you weren’t using containers.

Running Redis in a container

In this section, we’ll get the Red Hat Enterprise Linux 8 Redis application stream running in a container managed by systemd on the host system. Searching the Red Hat Container Catalog, we can look for Redis images. At the time this article was written, there wasn’t a Redis image based on UBI 8 in the Red Hat Container Catalog, but there was one based on RHEL 8. We’ll pull the image before running to make it easier to inspect it:

$ sudo podman pull registry.redhat.io/rhel8/redis-5

Because containers are designed to be ephemeral, we need to set up permanent storage for the Redis datastore. We’ll set up a directory on the host’s system and map it into the container. First, inspect the image to find out the user ID we’ll need for the directories:

$ sudo podman inspect redis-5 | grep -A 1 User

Alternatively, we could also get information about this image from the Red Hat Container Catalog page.

Now that we’ve got the numeric User ID the container will run under (1001), create a directory on the host, give that User ID ownership, and set the context for SELinux:

$ sudo mkdir -p /opt/dbdata/node-redis-db
$ sudo chown 1001:1001 /opt/dbdata/node-redis-db
$ sudo setfacl -m u:1001:-wx /opt/dbdata/node-redis-db
$ sudo semanage fcontext -a -t container_file_t /opt/dbdata/node-redis-db
$ sudo restorecon -v /opt/dbdata/node-redis-db

Let’s test Redis by hand:

$ sudo podman run -it --name node-redis-db -p 6379:6379 -v /opt/dbdata/node-redis-db:/var/lib/redis/data:Z registry.redhat.io/rhel8/redis-5

You should see output that looks like this:

---> 22:00:01     Processing Redis configuration files ...
---> 22:00:01     WARNING: setting REDIS_PASSWORD is recommended
---> 22:00:01     Sourcing post-init.sh ...
---> 22:00:01     Cleaning up environment variable REDIS_PASSWORD ...
---> 22:00:01     Running final exec -- Only Redis logs after this point
1:C 26 Aug 2019 22:00:01.568 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 26 Aug 2019 22:00:01.568 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 26 Aug 2019 22:00:01.568 # Configuration loaded
                _._
           _.-``__ ''-._
      _.-``    `.  `_.  ''-._           Redis 5.0.3 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 1
  `-._    `-._  `-./  _.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |           http://redis.io
  `-._    `-._`-.__.-'_.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |
  `-._    `-._`-.__.-'_.-'    _.-'
      `-._    `-.__.-'    _.-'
          `-._        _.-'
              `-.__.-'

Redis started correctly, so you can use Ctrl+C to stop the container. Clean up by removing the container:

$ sudo podman rm node-redis-db

Next, create a systemd unit file to manage Redis. As root, use an editor or cat > to create /etc/systemd/system/node-redis-db.service with the following contents:

[Unit]
Description=Node app Redis Database
After=network.target

[Service]
Type=simple
TimeoutStartSec=5m
ExecStartPre=-/usr/bin/podman rm "node-redis-db"

ExecStart=/usr/bin/podman run -it --name node-redis-db -e REDIS_PASSWORD=mysecret -p 6379:6379 -v /opt/dbdata/node-redis-db:/var/lib/redis/data:Z registry.redhat.io/rhel8/redis-5

ExecReload=-/usr/bin/podman stop "node-redis-db"
ExecReload=-/usr/bin/podman rm "node-redis-db"
ExecStop=-/usr/bin/podman stop "node-redis-db"
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target

Next, tell systemd to reload, start the Redis service, and then check the output:

$ sudo systemctl daemon-reload
$ sudo systemctl start node-redis-db
$ sudo systemctl status node-redis-db

You can check the container logs with:

$ sudo podman logs node-redis-db

The Redis port, 6379, is exposed to the host system, so if you have the Redis client installed, you should be able to connect to the Redis server.

There are a couple of things to note about the podman run command inside the systemd unit file. Don’t use a -d option to detach from the running container like you would from the command line. Because systemd is managing the process, podman run should not exit until the process inside the container dies. If you use -d, systemd will think the container has failed and will restart it.

The --rm option to podman run that automatically removes the containers when it exits isn’t used. Instead, systemd is configured to run a podman rm command just before starting the container. This setup gives you the opportunity to check the state of files inside the stopped container after it exits.

Testing Redis from the Node.js container

Now that Redis is running, we’ll test it from the Node.js container. Using an editor on your host system, create /opt/src/react-web-app/redis-test.js with the following contents:

let redis     = require('redis'),

client    = redis.createClient({
    port      : 6379,
    host      : '127.0.0.1',
    password  : 'mysecret',
});

count = client.incr('view-count', function(err) {
  if (err) {
    throw err; /* in production, handle errors more gracefully */
  } else {
    client.get('view-count',function(err,value) {
      if (err) {
        throw err;
      } else {
        console.log(value);
        process.exit();
      }
    }
  );
};
});

We need to run the test from inside a Node.js container. Run this command to start one:

$ sudo podman run --rm -it --user 1000:1000 -v /opt/src/react-web-app:/opt/app-root/src:Z --net host registry.redhat.io/ubi8/nodejs-10 /bin/bash

Now, using the bash shell inside the container, install Redis support:

$ npm install redis

You can now run the test app:

$ node redis-test.js

Every time you run the test app, the counter should increase. You could now create a back end for your Node.js application that uses Redis for storage. You can restart the Redis container using systemctl restart node-redis-db to verify that the Redis datastore is being retained across container restarts.

Using Buildah to create an image with your Node.js app

After you’ve developed your app, you can use Buildah to create a distributable container image with your Node.js app. While Buildah can use a Dockerfile, we’ll use Buildah command lines. This option is much more flexible for complex builds and automation. You can use shell scripts or whatever tools you use for your build environment.

In /opt/src/react-web-app, create app-image-build.sh with the following contents:

#!/bin/sh
# Build our Node.js app and all the dependencies into a container image
# Note: OOTB on RHEL 7.6 this needs to be run as root.

MYIMAGE=myorg/mynodeapp

USERID=1000

IMAGEID=$(buildah from ubi8/nodejs-10)

# any build steps above this line run as root
# after this build steps run as $USERID
buildah config --user $USERID:$USERID $IMAGEID

buildah copy $IMAGEID . /opt/app-root/src

# Any other prep steps go here

buildah config --cmd 'npm start' $IMAGEID

buildah commit $IMAGEID $MYIMAGE

Now, make app-image-buils.sh executable, then build the image:

$ chmod +x app-image-build.sh
$ sudo ./app-image-build.sh

Now you can run and test the new image:

$ sudo podman run --rm -it --net host myorg/mynodeapp

Note that the run command no longer needs the volume mount, because the code is now part of the container.

When you are ready, you can distribute your application to others by pushing it to a container registry like Red Hat’s Quay.io.

Managing your Node.js app with systemd

You can manage your new Node.js app with systemd so it will start on system boot. As root, create the systemd unit file /etc/systemd/system/my-node-app.service with the following contents:

[Unit]
Description=My Node App 
After=node-redis-db.service

[Service]
Type=simple
TimeoutStartSec=30s
ExecStartPre=-/usr/bin/podman rm "mynodeapp"

ExecStart=/usr/bin/podman run --name mynodeapp --net host myorg/mynodeapp

ExecReload=-/usr/bin/podman stop "myorg/mynodeapp"
ExecReload=-/usr/bin/podman rm "myorg/mynodeapp"
ExecStop=-/usr/bin/podman stop "myorg/mynodeapp"
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target

Tell systemd to reload, then start the app:

$ sudo systemctl daemon-reload
$ sudo systemctl start my-node-app
$ systemctl status my-node-app

You've now got your Node.js App and Redis running in containers and managed by systemd.

Next steps

By now you should see that it is pretty easy to get the software components you need running in containers so you can focus on development. It shouldn’t feel too different from developing without containers. Hopefully, you can see how to build on these instructions for your own apps.

You should check out what other UBI 8 images are available for you to use in the Red Hat Container Catalog. If the language, runtime, or server aren’t available as a UBI image, you can build your own starting with the ubi8 base image. Then you can add the application streams and other RPMs you need with yum commands in a Dockerfile, or with buildah run.

The setup in this article has a number of drawbacks, as it was intended to be a quick and easy-to-digest demo. There are many ways one could improve the setup. For example, the Node.js container with the packaged app is configured to share the host’s network with --net host, which makes it simple for the Node.js process to connect to Redis via localhost. While this choice is quick and easy for development, you don’t get the network isolation that containers offer.

One of the ways the network configuration could be improved on is to use Podman’s pod capabilities to put the web and database containers in the same pod, where they share namespaces. See Brent Baude’s article Podman: Managing pods and containers in a local container runtime.

Red Hat Universal Base Images

I’ve mentioned UBI several times in this article without defining the term. UBI are universal base images from Red Hat that you can use as a base for your container images. From Mike Guerette’s article, Red Hat Universal Base Image: How it works in 3 minutes or less:

"Red Hat Universal Base Images (UBI) are OCI-compliant container base operating system images with complementary runtime languages and packages that are freely redistributable. Like previous RHEL base images, they are built from portions of Red Hat Enterprise Linux. UBI images can be obtained from the Red Hat Container Catalog and be built and deployed anywhere.

"And, you don’t need to be a Red Hat customer to use or redistribute them. Really."

With the release of Red Hat Enterprise Linux 8 in May, Red Hat announced that all RHEL 8 base images would be available under the new Universal Base Image End User License Agreement (EULA). This means you can build and redistribute container images that use Red Hat’s UBI images as your base instead of having to switch to images based on other distributions, like Alpine. In other words, you won’t have to switch from using yum to using apt-get when building containers.

There are three base images for Red Hat Enterprise Linux 8. The standard one is called ubi, or more precisely, ubi8/ubi. This is the image used above and is the one you will probably use most often. The other two are minimal containers; they have very little supporting software in them for when image size is a high priority and a multi-service image that allows you to run multiple processes inside the container managed by systemd.

Note: There are also UBI images for Red Hat Enterprise Linux 7 under ubi7, if you want to build and distribute containers running on a RHEL 7 image. For this article, we only use the ubi8 images.

If you are just starting out with containers, you might not need to delve into UBI details right now. Just use the ubi8 images to build containers based on Red Hat Enterprise Linux 8. However, you will want to understand UBI details when you start distributing container images or have questions about support. For more information, see the references at the end of this article.

More information

Related articles:

Cheat sheets:

Podman and Buildah:

UBI: 

Last updated: April 3, 2023