Your submission was sent successfully! Close

You have successfully unsubscribed! Close

Thank you for signing up for our newsletter!
In these regular emails you will find the latest updates about Ubuntu and upcoming events where you can meet our team.Close

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;

    red INTEGER,
    green INTEGER,
    blue INTEGER,
    colorname VARCHAR NOT NULL

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


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);
  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
    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("<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

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'
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 /var/www/html/>
        AllowOverride None
        Require all granted

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

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

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-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 my-postgresql-oci
$ cd my-postgresql-oci/
$ git checkout origin/14-22.04 -b my-postgresql-oci-branch
$ find ./examples/ -type f

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:

        image: ubuntu/postgres:14-22.04_beta
            - 5432:5432
            - POSTGRES_PASSWORD=myS&cret
            - ./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/ 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 my-postgresql-oci
$ cd my-apache2-oci/
$ git checkout origin/2.4-22.04 -b my-apache2-oci-branch
$ find ./examples/ -type f

$ 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'

        image: ubuntu/apache2:2.4-22.04_beta
            - 8080:80
            - ./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


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


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.

This page was last modified 6 days ago. Help improve this document in the forum.