Docker Prompt: a Tool for Docker Compose Configuration

A simple command line tool to prompt and configure docker-compose stacks

Introduction

I manage a lot of small server setups, like one or two servers per location, and I’ve set up these servers using Docker and Docker-compose running on Ubuntu. I generally write scripts to handle everything from creating volumes and environment variables to starting, stopping, and updating the services. For the compose files, I typically split them into sections for different types of environments or configurations (such as FreeIPA master and replica), then use the COMPOSE_FILE variable to determine what gets included for each setup.

Problem

This approach is very modular and allows differently configured setups to use the same compose files, but it has many drawbacks. Writing .yml files for each volume or writing complex configuration scripts is time-consuming and error-prone. Additionally, if you happened to run a configuration script more than once, it would erase the current .env file, losing all your settings. For an example, see the mkenv configuration script for Gitea below.

  1#!/bin/bash
  2YOUR_BUILD="compose/core.yml"
  3echo -n "Enter your domain: "
  4read YOUR_DOMAIN
  5
  6echo -n "Enter your SSH port: "
  7read YOUR_SSH_LISTEN_PORT
  8
  9
 10echo "You can use NFS, (abs)olute path or (rel)ative path"
 11echo "If you want to mix them create your own compose/custom.yml file"
 12PS3="Which do you want to use?: "
 13select choice in "NFS" "Absolute Path" "Relative Path"
 14do
 15    case $choice in
 16        "NFS")
 17            YOUR_BUILD="${YOUR_BUILD}:compose/nfs.yml"
 18            echo -n "Enter NFS path to your Gitea data directory: "
 19            read IN
 20            IFS=':' read -ra ADDR <<< "$IN"
 21            GITEA_DATA_VOL_IP=${ADDR[0]}
 22            GITEA_DATA_VOL_DIR=${ADDR[1]}
 23
 24            echo -n "Enter NFS path to your Gitea database directory: "
 25            read IN
 26            IFS=':' read -ra ADDR <<< "$IN"
 27            GITEA_DATABASE_VOL_IP=${ADDR[0]}
 28            GITEA_DATABASE_VOL_DIR=${ADDR[1]}
 29
 30            break;;
 31        "Absolute Path")
 32            YOUR_BUILD="${YOUR_BUILD}:compose/local.yml"
 33            echo -n "Enter the absolute path to your Gitea data directory: "
 34            read GITEA_DATA_VOL_DIR
 35
 36            echo -n "Enter the absolute path to your Gitea database directory: "
 37            read GITEA_DATABASE_VOL_DIR
 38
 39            break;;
 40        "Relative Path")
 41            YOUR_BUILD="${YOUR_BUILD}:compose/local.yml"
 42            GITEA_DATA_VOL_DIR="\${PWD}/data"
 43            echo "Enter your gitea data directory relative to \"${PWD}\""
 44            echo "Default location is ${GITEA_DATA_VOL_DIR}"
 45            echo -n "Enter your Gitea data directory: "
 46            read IN
 47            if [ ! -z "$IN" ]; then
 48                GITEA_DATA_VOL_DIR="\${PWD}/$IN"
 49            fi
 50
 51            GITEA_DATABASE_VOL_DIR="\${PWD}/database"
 52            echo "Enter your gitea database directory relative to \"${PWD}\""
 53            echo "Default location is ${GITEA_DATABASE_VOL_DIR}"
 54            echo -n "Enter your Gitea database directory: "
 55            read IN
 56            if [ ! -z "$IN" ]; then
 57                GITEA_DATABASE_VOL_DIR="\${PWD}/$IN"
 58            fi
 59
 60            break;;
 61        *)
 62           echo "Invalid response";;
 63    esac
 64done
 65
 66echo -n "Do you want to add your own changes in 'compose/custom.yml'? (y/n): "
 67read CUSTOM
 68if [ "$CUSTOM" == "y" ] || [ "$CUSTOM" == "Y" ]; then
 69    YOUR_BUILD="${YOUR_BUILD}:compose/custom.yml"
 70    touch compose/custom.yml
 71fi
 72
 73echo -n "Do you want to prefix the compose setup? (y/n): "
 74read IN
 75if [ "$IN" == "y" ] || [ "$IN" == "Y" ]; then
 76    echo -n "Enter your prefix: "
 77    read PREFIX
 78    PREFIX="${PREFIX}_"
 79fi
 80
 81echo "Generating database password"
 82YOUR_DB_PASSWORD=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 22 | head -n 1)
 83
 84echo "Getting UID"
 85YOUR_USER_UID=$(id -u)
 86
 87echo "Getting GID"
 88YOUR_USER_GID=$(id -g)
 89
 90sed s/YOUR_DB_PASSWORD/$YOUR_DB_PASSWORD/g example.env | \
 91sed s/YOUR_USER_UID/"$YOUR_USER_UID"/g |\
 92sed s/YOUR_USER_GID/"$YOUR_USER_GID"/g |\
 93sed s/YOUR_PREFIX/$PREFIX/g |\
 94sed s,YOUR_BUILD,$YOUR_BUILD,g |\
 95sed s/YOUR_SSH_LISTEN_PORT/$YOUR_SSH_LISTEN_PORT/g |\
 96sed s,YOUR_GITEA_DATA_VOL_IP,$GITEA_DATA_VOL_IP,g |\
 97sed s,YOUR_GITEA_DATABASE_VOL_IP,$GITEA_DATABASE_VOL_IP,g |\
 98sed s,YOUR_GITEA_DATA_VOL_DIR,$GITEA_DATA_VOL_DIR,g |\
 99sed s,YOUR_GITEA_DATABASE_VOL_DIR,$GITEA_DATABASE_VOL_DIR,g |\
100sed s/YOUR_DOMAIN/$YOUR_DOMAIN/g > .env

Solution

To avoid writing long scripts for setting up docker-compose files, I developed a simple tool to read a definition file and an optional .env file and then ask for what’s missing while still allowing the user to keep existing settings intact. The goals for this are: Don’t depend on any tools being installed on the server since many scripts require many commands to be present to work Handle already existing .env files and don’t lose settings Use a simple definition language that will take little effort to write To not depend on local tools, I implemented the tool in Go; this allows a single static binary to run everywhere. For handling existing .env files, the trick was to read the file into a string map and use these settings as the defaults for each prompt.

Custom DSL

For the definition language, since I figured it would be a simple task, I just defined a simple line-based language as seen below:

1DATABASE_PASSWORD=@:passgen
2PROTOCOL=@:select(tcp,udp)
3UID=@:uid
4GID=@:gid
5ADMIN_NAME=@:text_input

JSON Instead

This simple Domain-specific language (DSL) quickly became a bad idea. It would require more work to implement than I wanted, so I decided to go with JSON, something familiar to most developers, and then add a few functions to generate needed information, such as passwords or user IDs. The final JSON at the time of writing this article looks as seen below:

  1{
  2  "version": 1,
  3  "configurations": {
  4    ".env": {
  5      "file": ".env",
  6      "variables": [
  7        {
  8          "prompt": "Enter prefix if wanted",
  9          "function": "text_input",
 10          "defaults": [""],
 11          "outputs": ["PREFIX"],
 12          "options": [],
 13          "affixes": ["", "_"]
 14        },
 15        {
 16          "function": "bool_input",
 17          "compose_files": ["compose/core.yml"],
 18          "defaults": ["true"]
 19        },
 20        {
 21          "prompt": "would you like to add custom.yml to your compose?",
 22          "function": "bool_input",
 23          "compose_files": ["compose/custom.yml"],
 24          "outputs": ["CUSTOM_COMPOSE_FILE"],
 25          "defaults": ["false"]
 26        },
 27        {
 28          "prompt": "Enter domain name",
 29          "function": "text_input",
 30          "defaults": [""],
 31          "outputs": ["DOMAIN"],
 32          "options": []
 33        },
 34        {
 35          "prompt": "Compose project name",
 36          "function": "text_input",
 37          "defaults": ["${PREFIX}gitea"],
 38          "outputs": ["COMPOSE_PROJECT_NAME"],
 39          "options": []
 40        },
 41        {
 42          "prompt": "Database type",
 43          "function": "select_input",
 44          "defaults": [],
 45          "outputs": ["DB_TYPE"],
 46          "options": ["postgres"]
 47        },
 48        {
 49          "prompt": "Database name",
 50          "function": "text_input",
 51          "defaults": ["gitea"],
 52          "outputs": ["POSTGRES_DB", "DB_NAME"],
 53          "options": []
 54        },
 55        {
 56          "prompt": "PostgreSQL user",
 57          "function": "text_input",
 58          "defaults": ["gitea"],
 59          "outputs": ["POSTGRES_USER", "DB_USER"],
 60          "options": []
 61        },
 62        {
 63          "prompt": "PostgreSQL password",
 64          "function": "password_input",
 65          "defaults": ["@passgen(30)"],
 66          "outputs": ["POSTGRES_PASSWORD", "DB_PASSWD"],
 67          "options": []
 68        },
 69        {
 70          "prompt": "Database host",
 71          "function": "",
 72          "defaults": ["${PREFIX}gitea_database:5432"],
 73          "outputs": ["DB_HOST"],
 74          "options": []
 75        },
 76        {
 77          "prompt": "Landing page",
 78          "function": "select_input",
 79          "defaults": [],
 80          "outputs": ["LANDING_PAGE"],
 81          "options": ["explore"]
 82        },
 83        {
 84          "prompt": "Disable registration",
 85          "function": "bool_input",
 86          "comments": [
 87            "This is used to disable registration on the landing page",
 88            "If you set it to false, you will be able to register new users"
 89          ],
 90          "defaults": ["true"],
 91          "outputs": ["DISABLE_REGISTRATION"],
 92          "options": []
 93        },
 94        {
 95          "prompt": "SSH port",
 96          "function": "int_input",
 97          "defaults": ["22222"],
 98          "outputs": [
 99            "SSH_LISTEN_PORT",
100            "GITEA__server__SSH_PORT",
101            "GITEA__server__SSH_LISTEN_PORT"
102          ],
103          "options": []
104        },
105        {
106          "prompt": "Gitea domain name",
107          "function": "text_input",
108          "defaults": ["gitea.${DOMAIN}"],
109          "outputs": [
110            "GITEA_DOMAIN",
111            "GITEA__server__SSH_DOMAIN",
112            "GITEA__server__DOMAIN"
113          ],
114          "options": []
115        },
116        {
117          "prompt": "Gitea root url",
118          "function": "text_input",
119          "defaults": ["https://gitea.${DOMAIN}"],
120          "outputs": ["GITEA__server__ROOT_URL"],
121          "options": []
122        },
123        {
124          "prompt": "User uid",
125          "function": "id_input",
126          "defaults": ["@uid"],
127          "outputs": ["USER_UID"],
128          "options": []
129        },
130        {
131          "prompt": "User gid",
132          "function": "id_input",
133          "defaults": ["@gid"],
134          "outputs": ["USER_GID"],
135          "options": []
136        },
137        {
138          "prompt": "Data volume",
139          "function": "volume_input",
140          "defaults": ["${PWD}/data", ""],
141          "outputs": ["GITEA_DATA_VOL_DIR", "GITEA_DATA_VOL_IP"],
142          "options": [],
143          "key": "data",
144          "name": "${PREFIX}gitea_data"
145        },
146        {
147          "prompt": "Database volume",
148          "function": "volume_input",
149          "defaults": ["${PWD}/database", ""],
150          "outputs": ["GITEA_DATABASE_VOL_DIR", "GITEA_DATABASE_VOL_IP"],
151          "options": [],
152          "key": "database",
153          "name": "${PREFIX}gitea_database"
154        }
155      ]
156    }
157  }
158}

Done

After switching to JSON and defining how the Schema (as seen above) should look, I finished writing the initial version of docker-prompt and then tested it by porting a few of my docker-compose configurations to its JSON definition language; this worked well and achieved all the goals.

Source code

The source for docker-prompt is available for use on Gitlab under the Mozilla Public License Version 2.0. Contributions are welcome; feel free to create a pull request.