Instances - Running Services

Apptainer is most commonly used to run containers interactively, or in a batch job, where the container runs in the foreground, performs some work, and then exits. There are different ways in which you can run Apptainer containers in the foreground. If you use run, exec and shell to interact with processes in the container, then you are running Apptainer containers in the foreground.

Apptainer, also allows you to run containers in a “detached” or “daemon” mode where the container runs a service. A “service” is essentially a process running in the background that multiple different clients can use. For example, a web server or a database.

A Apptainer container running a service in the background is called an instance, to distinguish it from the default mode which runs containers in the foreground.

Overview

This page will help you understand instances using an elementary example followed by a more useful example running an NGINX web server using instances. At the end, you will find a more detailed example of running an instance of a service exposing an API that converts URL to PDFs.

To run a service, such as a web server, outside of a container you would typically install the package for the web server and then instruct systemd (which manages system services on most Linux distributions) to start it. E.g.

$ sudo dnf install nginx
$ sudo systemctl enable --now nginx

If you were to attempt this in a container, it’s likely that it will not work as expected. You cannot use systemd to run services in a container, by default. It expects various conditions that are satisfied on the host system, but not inside containers.

Instead, you should run NGINX in the foreground inside the container, but then run the container in the background, as an instance.

Container Instances in Apptainer

To demonstrate the basics of instances, let’s use an easy (though somewhat useless) example, using an Alpine Linux image from Apptainer’s github container registry:

$ apptainer pull oras://ghcr.io/apptainer/alpine:latest

The above command will save the alpine image as alpine_latest.sif.

Starting Instances

To start an instance, you should follow this procedure:

[command]                      [image]              [name of instance]

$ apptainer instance start   alpine_latest.sif     instance1

This command causes Apptainer to create an isolated environment for the container services to live inside. It will execute the contents of the startscript file which can be defined when you build the container via the def file. You can also use the instance run command if you want the container to execute the runscript when the instance initiates.

You can confirm that an instance is running by using the instance list command:

$ apptainer instance list

INSTANCE NAME    PID      IP              IMAGE
instance1        22084                    /home/dave/instances/alpine_latest.sif

Note

Instances are linked to the user account that started them. This means that if you use sudo to start an instance as root, you will need to use sudo for all commands managing that instance. instance list will not show instances started by other users.

If you want to run multiple instances from the same image, it’s as simple as running the command multiple times with different instance names. The instance name uniquely identify instances, so they cannot be repeated.

$ apptainer instance start alpine_latest.sif instance2
$ apptainer instance start alpine_latest.sif instance3

We now have 3 instances, all using the same image:

$ apptainer instance list
INSTANCE NAME    PID      IP              IMAGE
instance1        22084                    /home/dave/instances/alpine_latest.sif
instance2        22443                    /home/dave/instances/alpine_latest.sif
instance3        22493                    /home/dave/instances/alpine_latest.sif

You can filter the instance list by supplying a pattern:

$ apptainer instance list '*2'
INSTANCE NAME    PID      IP              IMAGE
instance2        22443                    /home/dave/instances/alpine_latest.sif

When an instance is started, it will begin to run the %startscript from the container’s definition file in the background. If there is no %startscript the container will stay idle in the background.

You can also define start scripts on a per app basis.

$ apptainer instance start --app myapp alpine_latest.sif myapp-instance

When this app instance is started, it will begin to run the %myappstart from the container’s definition file in the background. If there is no %myappstart the container will stay idle in the background.

Besides how they are started, app instances behave just like regular instances.

Interacting With Instances

Although an instance runs its %startscript (if there is one) in the background, you can also interact with it in the foreground, by referring to it with an instance://<name> URI, where <name> is replaced with the instance name.

To run a specific command against an instance, in the foreground, use apptainer exec:

$ apptainer exec instance://instance1 cat /etc/os-release

Similarly, you can use apptainer run to run the %runscript for the container, against a running instance:

$ apptainer run instance://instance2

If you want to poke around inside of your instance, you can use the normal apptainer shell command, but give it the instance URI:

$ apptainer shell instance://instance3
Apptainer>

Stopping Instances

When you are finished with your instance you can clean it up with the instance stop command as follows:

$ apptainer instance stop instance1

If you have multiple instances running and you want to stop all of them, you can do so with a wildcard or the –all flag. The following three commands are identical.

$ apptainer instance stop '*'

$ apptainer instance stop --all

$ apptainer instance stop -a

Nginx “Hello-world” in Apptainer

The above example, although not very useful, should serve as a fair introduction to the concept of Apptainer instances and running containers in the background. We will now look at a more useful example of setting up an NGINX web server using instances. First we will create a basic definition file (let’s call it nginx.def):

Bootstrap: docker
From: nginx

%startscript
   nginx

This downloads the official NGINX Docker container, converts it to a Apptainer image, and tells it to run the nginx command when you start the instance. Because we are running a web server, which defaults to listening on privileged port 80, we’re going to run the following instance commands as root, using sudo.

$ apptainer build nginx.sif nginx.def
...
$ sudo apptainer instance start --writable-tmpfs nginx.sif web

The --writable-tmpfs option is needed, because NGINX will attempt to write some files when it starts up. --writable-tmpfs allows these to be written to a temporary, in-memory location, that will be removed when the instance is stopped.

Just like that we’ve downloaded, built, and run an NGINX Apptainer image. We can confirm it’s running using the curl tool, to fetch the web page that is now being hosted by NGINX.

$ curl localhost

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
 body {
     width: 35em;
     margin: 0 auto;
     font-family: Tahoma, Verdana, Arial, sans-serif;
 }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

You could also visit http://localhost in a web browser, if you are running the instance from a desktop session.

API Server Example

Let’s now package a useful service into a SIF container, and run it as an instance. The service we will be packaging is an API server that converts a web page into a PDF, and can be found here.

Building the image

To package the Web to PDF service into a SIF container, we must create a definition file. Let’s first choose a base from which to build our container. In this case the docker image node:8 which comes pre-installed with Node 8 has been used:

Bootstrap: docker
From: node:8

The service also requires a slew of dependencies to be manually installed in addition to Node 8, so we can add those into the post section as well as calling the installation script for the url-to-pdf:

%post

    apt-get update && apt-get install -yq gconf-service libasound2 \
        libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 \
        libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 \
        libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 \
        libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
        libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 \
        libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
        fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils \
        wget curl && rm -r /var/lib/apt/lists/*
    git clone https://github.com/alvarcarto/url-to-pdf-api.git pdf_server
    cd pdf_server
    npm install
    touch .env
    chmod -R 0755 .
    cp .env.sample .env

We need to define what happens when we start an instance of the container by writing a %startscript. In this situation, we want to run the commands that start up the url-to-pdf service:

%startscript
    cd /pdf_server
    npm start

Also, the url-to-pdf service requires some environment variables to be set, which we can do in the environment section:

%environment
    NODE_ENV=development
    PORT=9000
    ALLOW_HTTP=true
    URL=localhost
    export NODE_ENV PORT ALLOW_HTTP URL

The complete definition file will look like this:

Bootstrap: docker
From: node:8

%post

    apt-get update && apt-get install -yq gconf-service libasound2 \
        libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 \
        libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 \
        libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 \
        libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
        libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 \
        libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
        fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils \
        wget curl && rm -r /var/lib/apt/lists/*
    git clone https://github.com/alvarcarto/url-to-pdf-api.git pdf_server
    cd pdf_server
    npm install
    touch .env
    chmod -R 0755 .
    cp .env.sample .env

%startscript
    cd /pdf_server
    npm start

%environment
    NODE_ENV=development
    PORT=9000
    ALLOW_HTTP=true
    URL=localhost
    export NODE_ENV PORT ALLOW_HTTP URL

We can now build the container image from the definition file:

$ apptainer build url-to-pdf.sif url-to-pdf.def

Running the Service

We can now start an instance to run the service:

$ apptainer instance start url-to-pdf.sif pdf

Because the service listens on port 9000, which is not a privileged port, we don’t need to run it with sudo this time.

We can confirm it’s working by sending the server an http request using curl:

$ curl -o apptainer.pdf localhost:9000/api/render?url=http://apptainer.org/docs
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 64753  100 64753    0     0  19663      0  0:00:03  0:00:03 --:--:-- 19669

You should see a PDF file being generated like the one shown below:

Screenshot of the PDF generated!

If you shell into the instance, you can see the processes that are running, to provide the service:

$ apptainer shell instance://pdf
Apptainer> ps aux
USER     PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
user       1  0.0  0.0 1178984 20700 ?       Sl   11:40   0:00 sinit
user      13  0.0  0.0   4284   696 ?        S    11:40   0:00 /bin/sh /.singularity.d/startscript
user      15  1.0  0.0 984908 41508 ?        Sl   11:40   0:00 npm
user      25  0.0  0.0   4292   716 ?        S    11:40   0:00 sh -c env-cmd nodemon --watch ./src -e js src/index.js
user      26  0.1  0.0 876908 31084 ?        Sl   11:40   0:00 node /pdf_server/node_modules/.bin/env-cmd nodemon --watch ./src -e js src/index
user      32  0.7  0.0 1113984 39976 ?       Sl   11:40   0:00 node /pdf_server/node_modules/.bin/nodemon --watch ./src -e js src/index.js
user      44  1.7  0.0 941556 53804 ?        Sl   11:40   0:00 /usr/local/bin/node src/index.js
user     124  0.0  0.0  18372  3592 pts/1    S    11:41   0:00 /bin/bash --norc
user     130  0.0  0.0  36640  2836 pts/1    R+   11:41   0:00 ps aux

Instance Logs

Generally, when running services using instances, we write the %startscript so that the service will run in the foreground, and would write any log messages to the terminal. When an instance container is started there is no terminal. Apptainer moves the container into the background, and collects output and error messages into log files.

You can view the location of log files for running instances using the --log option of the instance list command:

$ apptainer instance list --logs
INSTANCE NAME    PID       LOGS
pdf              935864    /home/user/.apptainer/instances/logs/mini/user/pdf.err
                           /home/user/.apptainer/instances/logs/mini/user/pdf.out

Note that the log files are located under .apptainer/instances in the user’s home directory, and are grouped by the hostname, and instance name.

The .out log collects standard output. The .err log collects standard error. You can look at the content of the log files to check how your service is running:

$ cat /home/user/.apptainer/instances/logs/mini/user/pdf.out

> url-to-pdf-api@1.0.0 start /pdf_server
> env-cmd nodemon --watch ./src -e js src/index.js

[nodemon] 1.19.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: /pdf_server/src/**/*
[nodemon] starting `node src/index.js`
2023-02-01T11:14:58.185Z - info: [app.js] ALLOW_HTTP=true, unsafe requests are allowed. Don't use this in production.
2023-02-01T11:14:58.187Z - info: [app.js] ALLOW_URLS set! Allowed urls patterns are:
2023-02-01T11:14:58.187Z - info: [app.js] Using CORS options: origin=*, methods=[GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH]
2023-02-01T11:14:58.206Z - warn: [router.js] Warning: no authentication required to use the API
2023-02-01T11:14:58.209Z - info: [index.js] Express server listening on http://localhost:9000/ in development mode
2023-02-01T11:15:17.269Z - info: [render-core.js] Rendering with opts: {
...

Resource Usage / Limits

If you are running a container as the root user, or your system supports cgroups v2, then all instances will be started inside a cgroup. A cgroup allows the resources used by the instance to be monitored, and limited.

To monitor the resource usage of an instance, use the instance stats command:

 $ apptainer instance stats pdf
INSTANCE NAME    CPU USAGE    MEM USAGE / LIMIT     MEM %    BLOCK I/O            PIDS
pdf              0.00%        479.8MiB / 62.2GiB    0.75%    470MiB / 131.6MiB    45

We can see that the instance is currently idle (0.00% CPU), and is using 479.8MiB of RAM. No limits have been applied, so the total RAM size of the machine is shown.

By default, instance stats is interactive when run from a terminal, and will update every second. To obtain point-in-time usage details use the --no-stream or --json options.

Where supported by the system’s cgroups configuration, resource limits can be applied to instances using the same command line flags that are available for interactive containers. E.g. to limit memory usage to 1GiB, we can use the --memory flag:

apptainer instance start --memory 1G url-to-pdf.sif pdf

System integration / PID files

If you are running services in containers you may want them to be started on boot, and shutdown gracefully automatically. This is usually performed by an init process, or another supervisor daemon installed on your host. Many init and supervisor daemons support managing processes via pid files.

You can specify a --pid-file option to apptainer instance start to write the PID for an instance to the specified file, e.g.

$ apptainer instance start --pid-file /home/dave/alpine.pid alpine_latest.sif instanceA

$ cat /home/dave/alpine.pid
23727

An example service file for an instance controlled by systemd is below. This can be used as a template to setup containerized services under systemd.

[Unit]
Description=Web Instance
After=network.target

[Service]
Type=forking
Restart=always
User=www-data
Group=www-data
PIDFile=/run/web-instance.pid
ExecStart=/usr/local/bin/apptainer instance start --pid-file /run/web-instance.pid /data/containers/web.sif web-instance
ExecStop=/usr/local/bin/apptainer instance stop web-instance

[Install]
WantedBy=multi-user.target

Note that Type=forking is required here, since instance start starts an instance and then exits.