Multi-node ROCK configuration with Docker-Compose

The prior section explained the use of a single container for running a single software instance, but the principle benefit of using ROCKs is the ability to easily create and architecturally organize, or “orchestrate”, them to operate together in a modular fashion.

If you set up a VM while following that section, you can continue to use that here, or if not feel free to create a new VM for this section, using those same directions.

Colors Web App

This section will demonstrate use of docker-compose to set up two nodes that inter-operate to implement a trivial CGI web app that lets the user select a background color from the standard rgb.txt color codes. Here’s the table definition itself:

$ cat > ~/my-color-database.sql <<'EOF'
CREATE DATABASE my_color_db;

CREATE TABLE "color"
(
    id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    red INTEGER,
    green INTEGER,
    blue INTEGER,
    colorname VARCHAR NOT NULL
);

REVOKE ALL ON "color" FROM public;
GRANT SELECT ON "color" TO "postgres";

EOF

For the data, we’ll scarf up X11’s rgb.txt file, which should be readily at hand with most Ubuntu desktop installations:

$ sudo apt-get install x11-common
$ grep -v ^! /usr/share/X11/rgb.txt | \
  awk 'BEGIN{print "INSERT INTO color(red, green, blue, colorname) VALUES"}
      $1 != $2 || $2 != $3 {
          printf("    (%d, %d, %d, '\''", $1, $2, $3);
          for (i = 4; i <= NF; i++) {
              printf("%s", $i);
          }
          printf("'\''),\n");
     }
  END  {print "    (0, 0, 0, '\''black'\'');"}' >> ~/my-color-database.sql

Here’s the corresponding CGI script:

$ cat > ~/my-colors.cgi <<'EOF'
#!/usr/bin/env python3

import cgi
import psycopg2

# Get web form data (if any)
query_form = cgi.FieldStorage()
if 'bgcolor' in query_form.keys():
    bgcolor = query_form["bgcolor"].value
else:
    bgcolor = 'FFFFFF'

print("Content-Type: text/html\n\n");

# Head
body_style = "body { background-color: #%s; }" %(bgcolor)
text_style = ".color-invert { filter: invert(1); mix-blend-mode: difference; }"
print(f"<html>\n<head><style>\n{body_style}\n{text_style}\n</style></head>\n")
print("<body>\n<h1 class=\"color-invert\">Pick a background color:</h1>\n")
print("<table width=\"500\" cellspacing=\"0\" cellpadding=\"0\">\n")
print("  <tr><th width=\"50\">Color</th><th>Name</th><th width=\"100\">Code</th></tr>\n")

# Connect database
db = psycopg2.connect(host='examples_postgres_1', user='postgres', password='myS&cret')

# Display the colors
colors = db.cursor()
colors.execute("SELECT * FROM color;")
for row in colors.fetchall():
    code = ''.join('{:02X}'.format(a) for a in row[1:4])
    color = row[4]
    print(f"  <tr style=\"background-color:#{code}\">\n")
    print(f"    <td><a href=\"my-colors.cgi?bgcolor={code}\">{color}</td>\n")
    print(f"    <td>{code}</td></tr>\n")

# Foot
print("</table>\n")
print("</body>\n</html>\n")
EOF

By default, Apache2 is configured to allow CGI scripts in the /usr/lib/cgi-bin system directory, but rather than installing the script there, let’s use our own directory to serve from:

$ cat > ~/my-apache.conf <<'EOF'
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
ErrorLog ${APACHE_LOG_DIR}/error.log
ServerName localhost
HostnameLookups Off
LogLevel warn
Listen 80

# Include module configuration:
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf

<Directory />
        AllowOverride None
        Require all denied
</Directory>

<Directory /var/www/html/>
        AllowOverride None
        Require all granted
</Directory>

<Directory /var/www/cgi-bin/>
        AddHandler cgi-script .cgi
        AllowOverride None
        Options +ExecCGI -MultiViews
        Require all granted
</Directory>

<VirtualHost *:80>
        DocumentRoot /var/www/html/
        ScriptAlias /cgi-bin/ /var/www/cgi-bin/
</VirtualHost>
EOF

Install Docker Compose

With our web app developed, we’re ready to containerize it. We’ll install Docker Compose, pull in the two base images for the database and web server, and create our own containers with our web app files and configuration layered on top.

First, install what we’ll need:

$ sudo apt-get update
$ sudo apt-get install -y docker.io docker-compose

Create Database Container

Next, prepare the Postgres container. Each of Ubuntu’s Docker Images has a git repository, referenced from the respective Docker Hub page. These repositories include some example content that we can build from:

$ git clone https://git.launchpad.net/~ubuntu-docker-images/ubuntu-docker-images/+git/postgresql my-postgresql-oci
$ cd my-postgresql-oci/
$ git checkout origin/14-22.04 -b my-postgresql-oci-branch
$ find ./examples/ -type f
./examples/README.md
./examples/postgres-deployment.yml
./examples/docker-compose.yml
./examples/config/postgresql.conf

Notice the two YAML files. The docker-compose.yml file lets us create a derivative container where we can insert our own customizations such as config changes and our own SQL data to instantiate our database. (The other YAML file is for Kubernetes-based deployments.)

$ mv -iv ~/my-color-database.sql ./examples/
renamed '/home/ubuntu/my-color-database.sql' -> './examples/my-color-database.sql'
$ git add ./examples/my-color-database.sql

Modify the services section of the file examples/docker-compose.yml to look like this:

services:
    postgres:
        image: ubuntu/postgres:14-22.04_beta
        ports:
            - 5432:5432
        environment:
            - POSTGRES_PASSWORD=myS&cret
        volumes:
            - ./config/postgresql.conf:/etc/postgresql/postgresql.conf:ro
            - ./my-color-database.sql:/docker-entrypoint-initdb.d/my-color-database.sql:ro

The volumes section of the file lets us bind files from our local git repository into our new container. Things like the postgresql.conf configuration file get installed to the normal system as you’d expect.

But the /docker-entrypoint-initdb.d/ directory will look unusual – this is a special directory provided by Ubuntu’s Postgres Docker container that will automatically run .sql (or .sql.gz or .sql.xz) and .sh files through the psql interpreter during initialization, in POSIX alphanumerical order. In our case we have a single .sql file that we want invoked during initialization.

Ubuntu’s ROCKs are also built with environment variables to customize behavior; above we can see where we can specify our own password.

Commit everything so far to our branch:

$ git commit -a -m "Add a color database definition"
[my-postgresql-oci-branch 0edeb20] Add a color database definition
 2 files changed, 549 insertions(+)
 create mode 100644 examples/my-color-database.sql

Now we’re ready to create and start our application’s database container:

$ cd ./examples/
$ sudo docker-compose up -d
Pulling postgres (ubuntu/postgres:edge)...
...
Creating examples_postgres_1 ... done

$ sudo docker-compose logs
...
postgres_1  | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/my-color-database.sql
...
postgres_1  | 2022-06-02 03:14:28.040 UTC [1] LOG:  database system is ready to accept connections

The -d flag causes the container to run in the background (you might omit it if you want to run it in its own window so you can watch the service log info live.)

Note that if there is an error, such as a typo in your .sql file, you can’t just re-run docker-compose up (or restart) because it’ll attempt to re-attach and may appear successful at first glance:

...
postgres_1  | psql:/docker-entrypoint-initdb.d/my-color-database.sql:10: ERROR:  type "sometypo" does not exist
postgres_1  | LINE 3:     "id" SOMETYPO,
postgres_1  |                  ^
examples_postgres_1 exited with code 3

$ sudo docker-compose up
Starting examples_postgres_1 ... done
Attaching to examples_postgres_1
postgres_1  |
postgres_1  | PostgreSQL Database directory appears to contain a database; Skipping initialization
...
postgres_1  | 2022-06-02 04:00:51.400 UTC [25] LOG:  database system was not properly shut down; automatic recovery in progress
...
postgres_1  | 2022-06-02 04:00:51.437 UTC [1] LOG:  database system is ready to accept connections

However, while there is a live database, our data didn’t load into it so it is invalid.

Instead, always issue a down command before attempting a restart when fixing issues:

$ sudo docker-compose down; sudo docker-compose up
...

Note that in our environment docker-compose needs to be run with root permissions; if it isn’t, you may see an error similar to this:

ERROR: Couldn't connect to Docker daemon at http+docker://localhost - is it running?
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.

At this point we could move on to the webserver container, but we can double-check our work so far by installing the Postgres client locally in the VM and running a sample query:

$ sudo apt-get install postgresql-client
$ psql -h localhost -U postgres
Password for user postgres: myS&cret
postgres=# \d
              List of relations
 Schema |     Name     |   Type   |  Owner
--------+--------------+----------+----------
 public | color        | table    | postgres
 public | color_id_seq | sequence | postgres
(2 rows)

postgres=# SELECT * FROM color WHERE id<4;
 id | red | green | blue | colorname
----+-----+-------+------+------------
  1 | 255 |   250 |  250 | snow
  2 | 248 |   248 |  255 | ghostwhite
  3 | 248 |   248 |  255 | GhostWhite
(3 rows)

Create Webserver Docker Container

Now we do the same thing for the Apache2 webserver.

Get the example files from Canonical’s Apache2 image repository via git:

$ cd ~
$ git clone https://git.launchpad.net/~ubuntu-docker-images/ubuntu-docker-images/+git/postgresql my-postgresql-oci
$ cd my-apache2-oci/
$ git checkout origin/2.4-22.04 -b my-apache2-oci-branch
$ find ./examples/ -type f
./examples/apache2-deployment.yml
./examples/README.md
./examples/docker-compose.yml
./examples/config/apache2.conf
./examples/config/html/index.html

$ mv -ivf ~/my-apache2.conf ./examples/config/apache2.conf
renamed '/home/ubuntu/my-apache2.conf' -> './examples/config/apache2.conf'
$ mv -iv ~/my-colors.cgi ./examples/
renamed '/home/ubuntu/my-colors.cgi' -> 'examples/my-colors.cgi'
$ chmod a+x ./examples/my-colors.cgi
$ git add ./examples/config/apache2.conf ./examples/my-colors.cgi

Modify the examples/docker-compose.yml file to look like this:

version: '2'

services:
    apache2:
        image: ubuntu/apache2:2.4-22.04_beta
        ports:
            - 8080:80
        volumes:
            - ./config/apache2.conf:/etc/apache2/apache2.conf:ro
            - ./config/html:/srv/www/html/index.html:ro
            - ./my-colors.cgi:/var/www/cgi-bin/my-colors.cgi:ro
        command: bash -c "apt-get update && apt-get -y install python3 python3-psycopg2; a2enmod cgid; apache2-foreground"
        restart: always

Commit everything to the branch:

$ git commit -a -m "Add a color CGI web application"

Now launch the web server container:

$ cd ./examples/
$ sudo docker-compose up -d

You will now be able to connect to the service:

$ firefox http://localhost:8080/cgi-bin/my-colors.cgi?bgcolor=FFDEAD

colors_screenshot_1

Click on one of the colors to see the background color change:

colors_screenshot_2

Once you’re done, if you wish you can cleanup the containers as before, or if you used Multipass you can shutdown and delete the VM:

$ exit
host> multipass stop my-vm
host> multipass delete my-vm

Next Steps

As you can see, docker-compose makes it convenient to set up multi-container applications without needing to perform runtime changes to the containers. As you can imagine, this can permit building a more sophisticated management system to handle fail-over, load-balancing, scaling, upgrading old nodes, and monitoring status. But rather than needing to implement all of this directly on top of docker-container, you can next investigate Kubernetes-style cluster management software such as microk8s.

1 Like

Is the pull from Git Lab? or Git Hub?

Having the pre settings of scripts for Apache2-- https://stackoverflow.com/questions/35430121/setting-up-apache2-to-execute-cgi-scripts
for the initial setup OK for the stack? As answered on link.
Worked for me or is this miss leading? I have Server and just learning.!
I am able to configure your method… Security matters.

Hi bernard010, thanks for trying the docs out. I’ve posted a new revision with a number of fixes and recommend starting again from the top.

Is the pull from Git Lab? or Git Hub?

Those pulls are from docker hub, actually.

Appreciate any further questions or feedback you may have.

1 Like

That title should read “Install docker-composer” I think.

Andreas, I’ve incorporated that change, and addressed your feedback on the other pages of the tutorial. Thanks again for the reviews!

1 Like

This has been explained in the previous section. I think you can just mention the directory here and refer to the previous explanation, and even improve that explanation while at it (this one does a good job at explaining how the .sql files are loaded, IMHO).

Hi!
There is a small mistake in the tutorial. The step of cloning the repository has a wrong url:

git clone https://git.launchpad.net/~canonical-server/ubuntu-docker-images/+git/postgresql my-postgresql-oci

Instead, it should be

git clone https://git.launchpad.net/~ubuntu-docker-images/ubuntu-docker-images/+git/postgresql my-postgresql-oci

The ~canonical-server part of the first path is wrong. Maybe it was an internal path? Anyway, the second command works as it should! :slight_smile:

Thank you for the notice, I updated the URLs. Indeed they are not working anymore, I think the repository was moved at some point.