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
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.