4 echo "Usage: launcher COMMAND CONFIG [--skip-prereqs] [--docker-args STRING]"
6 echo " start: Start/initialize a container"
7 echo " stop: Stop a running container"
8 echo " restart: Restart a container"
9 echo " destroy: Stop and remove a container"
10 echo " enter: Open a shell to run commands inside the container"
11 echo " logs: View the Docker logs for a container"
12 echo " bootstrap: Bootstrap a container for the config based on a template"
13 echo " run: Run the given command with the config in the context of the last bootstrapped image"
14 echo " rebuild: Rebuild a container (destroy old, bootstrap, start new)"
15 echo " cleanup: Remove all containers that have stopped for > 24 hours"
16 echo " start-cmd: Generate docker command used to start container"
19 echo " --skip-prereqs Don't check launcher prerequisites"
20 echo " --docker-args Extra arguments to pass when running docker"
21 echo " --skip-mac-address Don't assign a mac address"
22 echo " --run-image Override the image used for running the container"
26 # for potential re-exec later
32 # user_args_argv is assigned once when the argument vector is parsed.
34 # user_args is mutable: its value may change when templates are parsed.
35 # Superset of user_args_argv.
40 if [[ $command == "run" ]]; then
44 while [ ${#} -gt 0 ]; do
57 user_args
="$user_args_argv"
69 if [ -z "$command" -o -z "$config" -a "$command" != "cleanup" ]; then
73 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
74 re
='[[:upper:]/ !@#$%^&*()+~`=]'
75 if [[ $config =~
$re ]];
78 echo "ERROR: Config name '$config' must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
85 docker_min_version
='17.03.1'
86 docker_rec_version
='17.06.2'
87 git_min_version
='1.8.0'
88 git_rec_version
='1.8.0'
90 config_file
=containers
/"$config".yml
91 cidbootstrap
=cids
/"$config"_bootstrap.cid
92 local_discourse
=local_discourse
93 image
="discourse/base:2.0.20201221-2020"
94 docker_path
=`which docker.io 2> /dev/null || which docker`
97 if [ "${SUPERVISED}" = "true" ]; then
98 restart_policy
="--restart=no"
100 attach_on_run
="-a stdout -a stderr"
105 if [ -n "$DOCKER_HOST" ]; then
106 docker_ip
=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
107 elif [ -x "$(which ip 2>/dev/null)" ]; then
108 docker_ip
=`ip addr show docker0 | \
110 awk '{ split($2,a,"/"); print a[1] }';`
112 docker_ip
=`ifconfig | \
113 grep -B1 "inet addr" | \
114 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
116 awk -F: '{ print $3 }';`
119 # From https://stackoverflow.com/a/44660519/702738
121 if [[ $1 == $2 ]]; then
125 local i a
=(${1%%[^0-9.]*}) b
=(${2%%[^0-9.]*})
126 local arem
=${1#${1%%[^0-9.]*}} brem=${2#${2%%[^0-9.]*}}
127 for ((i
=0; i
<${#a[@]} || i
<${#b[@]}; i
++)); do
128 if ((10#${a[i]:-0} < 10#${b[i]:-0})); then
130 elif ((10#${a[i]:-0} > 10#${b[i]:-0})); then
134 if [ "$arem" '<' "$brem" ]; then
136 elif [ "$arem" '>' "$brem" ]; then
144 echo "Docker is not installed, you will need to install Docker in order to run Launcher"
145 echo "See https://docs.docker.com/installation/"
150 # Add a single retry to work around dockerhub TLS errors
151 $docker_path pull
$image ||
$docker_path pull
$image
156 if [ -z $docker_path ]; then
160 # 1. docker daemon running?
161 # we send stderr to /dev/null cause we don't care about warnings,
162 # it usually complains about swap which does not matter
163 test=`$docker_path info 2> /dev/null`
164 if [[ $?
-ne 0 ]] ; then
165 echo "Cannot connect to the docker daemon - verify it is running and you have access"
169 # 2. running an approved storage driver?
170 if ! $docker_path info
2> /dev
/null |
egrep -q 'Storage Driver: (aufs|zfs|overlay2)$'; then
171 echo "Your Docker installation is not using a supported storage driver. If we were to proceed you may have a broken install."
172 echo "overlay2 is the recommended storage driver, although zfs and aufs may work as well."
173 echo "Other storage drivers are known to be problematic."
174 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the 'Storage Driver' line."
176 echo "If you wish to continue anyway using your existing unsupported storage driver,"
177 echo "read the source code of launcher and figure out how to bypass this check."
181 # 3. running recommended docker version
182 test=($
($docker_path --version)) # Get docker version string
183 test=${test[2]//,/} # Get version alone and strip comma if exists
185 # At least minimum docker version
186 if compare_version
"${docker_min_version}" "${test}"; then
187 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
191 # Recommend newer docker version
192 if compare_version
"${docker_rec_version}" "${test}"; then
193 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
196 # 4. discourse docker image is downloaded
197 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
199 if [ -z "$test" ]; then
201 echo "WARNING: We are about to start downloading the Discourse base image"
202 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
204 echo "Please be patient"
210 # 5. running recommended git version
211 test=($
($git_path --version)) # Get git version string
212 test=${test[2]//,/} # Get version alone and strip comma if exists
214 # At least minimum version
215 if compare_version
"${git_min_version}" "${test}"; then
216 echo "ERROR: Git version ${test} not supported, please upgrade to at least ${git_min_version}, or recommended ${git_rec_version}"
220 # Recommend best version
221 if compare_version
"${git_rec_version}" "${test}"; then
222 echo "WARNING: Git version ${test} deprecated, recommend upgrade to ${git_rec_version} or newer."
225 # 6. able to attach stderr / out / tty
226 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
227 if [[ "$test" =~
"working" ]] ; then : ; else
228 echo "Your Docker installation is not working correctly"
230 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
234 # 7. enough space for the bootstrap on docker folder
235 folder
=`$docker_path info --format '{{.DockerRootDir}}'`
236 safe_folder
=${folder:-/var/lib/docker}
237 test=$
(($
(stat
-f --format="%a*%S" $safe_folder)/1024**3 < 5))
238 if [[ $test -ne 0 ]] ; then
239 echo "You have less than 5GB of free space on the disk where $safe_folder is located. You will need more space to continue"
242 if tty
>/dev
/null
; then
243 read -p "Would you like to attempt to recover space by cleaning docker images and containers in the system? (y/N)" -n 1 -r
245 if [[ $REPLY =~ ^
[Yy
]$
]]
247 $docker_path container prune
--force --filter until=1h
>/dev
/null
248 $docker_path image prune
--all --force --filter until=1h
>/dev
/null
249 echo "If the cleanup was successful, you may try again now"
257 if [ -z "$SKIP_PREREQS" ] && [ "$command" != "cleanup" ]; then
262 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
263 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
267 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
268 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
272 local templates
=`cat $1 | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
273 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
275 local arrTemplates
=${templates// / }
277 if [ ! -z "$templates" ]; then
278 for template
in "${arrTemplates[@]}"
280 local nested_templates
=$
(find_templates
$template)
282 if [ ! -z "$nested_templates" ]; then
283 templates
="$templates $nested_templates"
293 set_template_info
() {
294 templates
=$
(find_templates
$config_file)
296 arrTemplates
=(${templates// / })
297 config_data
=$
(cat $config_file)
301 for template
in "${arrTemplates[@]}"
303 [ ! -z $template ] && {
304 input
="$input _FILE_SEPERATOR_ $(cat $template)"
308 # we always want our config file last so it takes priority
309 input
="$input _FILE_SEPERATOR_ $config_data"
311 read -r -d '' env_ruby
<< 'RUBY'
314 input
=STDIN.readlines.
join
315 # default to UTF-8 for the dbs sake
316 env
= {'LANG' => 'en_US.UTF-8'}
317 input.
split('_FILE_SEPERATOR_').each
do |yml|
320 env.merge
!(YAML.load
(yml
)['env'] ||
{})
321 rescue Psych
::SyntaxError
=> e
329 env.each
{|k
,v| puts
"*ERROR." if v.is_a?
(Hash
)}
330 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
333 tmp_input_file
=$
(mktemp
)
335 echo "$input" > "$tmp_input_file"
336 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
338 rm -f "$tmp_input_file"
343 if [ "$i" == "*ERROR." ]; then
345 elif [ -n "$i" ]; then
346 env
[${#env[@]}]="${i//\{\{config\}\}/${config}}"
350 if [ "$ok" -ne 1 ]; then
352 echo "YAML syntax error. Please check your containers
/*.yml config files.
"
357 read -r -d '' labels_ruby << 'RUBY'
360 input=STDIN.readlines.join
362 input.split('_FILE_SEPERATOR_').each do |yml|
365 labels.merge!(YAML.load(yml)['labels'] || {})
366 rescue Psych::SyntaxError => e
374 puts labels.map{|k,v| "-l\n#{k}=#{v}" }.join("\n")
377 tmp_input_file
=$
(mktemp
)
379 echo "$input" > "$tmp_input_file"
380 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$labels_ruby"`
382 rm -f "$tmp_input_file"
387 if [ "$i" == "*ERROR." ]; then
389 elif [ -n "$i" ]; then
390 labels
[${#labels[@]}]=$
(echo $i |
sed s
/{{config
}}/${config}/g
)
394 if [ "$ok" -ne 1 ]; then
396 echo "YAML syntax error. Please check your containers/*.yml config files."
401 read -r -d '' ports_ruby
<< 'RUBY'
404 input
=STDIN.readlines.
join
406 input.
split('_FILE_SEPERATOR_').each
do |yml|
409 ports
+= (YAML.load
(yml
)['expose'] ||
[])
410 rescue Psych
::SyntaxError
=> e
418 puts ports.map
{ |p| p.to_s.include?
(':') ?
"-p\n#{p}" : "--expose\n#{p}" }.
join("\n")
421 tmp_input_file
=$
(mktemp
)
423 echo "$input" > "$tmp_input_file"
424 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$ports_ruby"`
426 rm -f "$tmp_input_file"
431 if [ "$i" == "*ERROR." ]; then
433 elif [ -n "$i" ]; then
434 ports
[${#ports[@]}]=$i
438 if [ "$ok" -ne 1 ]; then
440 echo "YAML syntax error. Please check your containers/*.yml config files."
447 if [ -z $docker_path ]; then
451 [ "$command" == "cleanup" ] && {
452 $docker_path container prune
--filter until=1h
453 $docker_path image prune
--all --filter until=1h
455 if [ -d /var
/discourse
/shared
/standalone
/postgres_data_old
]; then
457 echo "Old PostgreSQL backup data cluster detected taking up $(du -hs /var/discourse/shared/standalone/postgres_data_old | awk '{print $1}') detected"
458 read -p "Would you like to remove it? (y/N): " -n 1 -r && echo
460 if [[ $REPLY =~ ^
[Yy
]$
]]; then
461 echo "removing old PostgreSQL data cluster at /var/discourse/shared/standalone/postgres_data_old..."
462 rm -rf /var
/discourse
/shared
/standalone
/postgres_data_old
*
471 if [ ! "$command" == "setup" ]; then
472 if [[ ! -e $config_file ]]; then
473 echo "Config file was not found, ensure $config_file exists"
475 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
480 docker_version
=($
($docker_path --version))
481 docker_version
=${test[2]//,/}
482 restart_policy
=${restart_policy:---restart=always}
484 set_existing_container
(){
485 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
490 set_existing_container
492 if [ ! -z $existing ]
496 $docker_path stop
-t 30 $config
499 echo "$config was not started !"
500 echo "./discourse-doctor may help diagnose the problem."
506 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
507 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
509 if [ -n "$user_run_image" ]; then
510 run_image
=$user_run_image
511 elif [ -z "$run_image" ]; then
512 run_image
="$local_discourse/$config"
517 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
518 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
520 if [ -z "$boot_command" ]; then
522 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
523 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
525 if [ -z "$no_boot_command" ]; then
526 boot_command
="/sbin/boot"
534 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
535 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
537 if [[ -n "$docker_args" ]]; then
538 user_args
="$user_args_argv $docker_args"
544 if [ -z "$START_CMD_ONLY" ]
546 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
548 if [ ! -z $existing ]
550 echo "Nothing to do, your container has already started!"
554 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
555 if [ ! -z $existing ]
557 echo "starting up existing container"
560 $docker_path start
$config
572 # get hostname and settings from container configuration
573 for envar
in "${env[@]}"
575 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
577 # use as environment variable
583 hostname
=`hostname -s`
585 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
587 hostname
=$DISCOURSE_HOSTNAME
589 hostname
=$hostname-$config
592 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
593 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
594 # docker added more hostname rules
595 hostname
=${hostname//_/-}
598 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
599 mac_address
="--mac-address $($docker_path run $user_args -i --rm -a stdout -a stderr $image /bin/sh -c "echo $hostname |
md5sum |
sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/'")"
602 if [ ! -z "$START_CMD_ONLY" ] ; then
608 $docker_path run
--shm-size=512m
$links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
609 -e DOCKER_HOST_IP
="$docker_ip" --name $config -t "${ports[@]}" $volumes $mac_address $user_args \
610 $run_image $boot_command
624 (exec $docker_path run
--rm --shm-size=512m
$user_args $links "${env[@]}" -e DOCKER_HOST_IP
="$docker_ip" -i -a stdin
-a stdout
-a stderr
$volumes $run_image \
625 /bin
/bash
-c "$run_command") || ERR
=$?
627 if [[ $ERR > 0 ]]; then
633 # Is the image available?
634 # If not, pull it here so the user is aware what's happening.
635 $docker_path history $image >/dev
/null
2>&1 || pull_image
639 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
640 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
642 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
643 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
645 if [[ ! X
"" = X
"$base_image" ]]; then
654 run_command
="cd /pups &&"
655 if [[ ! "false" = $update_pups ]]; then
656 run_command
="$run_command git pull &&"
658 run_command
="$run_command /pups/bin/pups --stdin"
664 tmp_input_file
=$
(mktemp
)
666 echo "$input" > "$tmp_input_file"
667 (exec cat "$tmp_input_file" |
$docker_path run
--shm-size=512m
$user_args $links "${env[@]}" -e DOCKER_HOST_IP
="$docker_ip" --cidfile $cidbootstrap -i -a stdin
-a stdout
-a stderr
$volumes $image \
668 /bin
/bash
-c "$run_command") || ERR
=$?
670 rm -f "$tmp_input_file"
673 # magic exit code that indicates a retry
674 if [[ "$ERR" == 77 ]]; then
675 $docker_path rm `cat $cidbootstrap`
678 elif [[ "$ERR" > 0 ]]; then
682 if [[ $FAILED = "TRUE" ]]; then
683 if [[ ! -z "$DEBUG" ]]; then
684 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config-debug ||
echo 'FAILED TO COMMIT'
685 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
688 $docker_path rm `cat $cidbootstrap`
690 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one."
691 echo "./discourse-doctor may help diagnose the problem."
697 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
698 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
704 echo "Successfully bootstrapped, to startup use ./launcher start $config"
714 exec $docker_path exec -it $config /bin
/bash
--login
724 $docker_path logs
$config
746 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
747 echo "Ensuring launcher is up to date"
751 LOCAL
=$
(git rev-parse HEAD
)
752 REMOTE
=$
(git rev-parse @
{u
})
753 BASE
=$
(git merge-base HEAD @
{u
})
755 if [ $LOCAL = $REMOTE ]; then
756 echo "Launcher is up-to-date"
758 elif [ $LOCAL = $BASE ]; then
759 echo "Updating Launcher..."
760 git pull ||
(echo 'failed to update' && exit 1)
762 echo "Launcher updated, restarting..."
763 exec "$0" "${SAVED_ARGV[@]}"
765 elif [ $REMOTE = $BASE ]; then
766 echo "Your version of Launcher is ahead of origin"
769 echo "Launcher has diverged source, this is only expected in Dev mode"
774 set_existing_container
776 if [ ! -z $existing ]
778 echo "Stopping old container"
781 $docker_path stop
-t 60 $config
787 if [ ! -z $existing ]
789 echo "Removing old container"
792 $docker_path rm $config
802 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)