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."
86 docker_min_version
='17.03.1'
87 docker_rec_version
='17.06.2'
88 git_min_version
='1.8.0'
89 git_rec_version
='1.8.0'
91 config_file
=containers
/"$config".yml
92 cidbootstrap
=cids
/"$config"_bootstrap.cid
93 local_discourse
=local_discourse
94 image
="discourse/base:2.0.20220818-0047"
95 docker_path
=`which docker.io 2> /dev/null || which docker`
98 if [ "${SUPERVISED}" = "true" ]; then
99 restart_policy
="--restart=no"
101 attach_on_run
="-a stdout -a stderr"
106 if [ -n "$DOCKER_HOST" ]; then
107 docker_ip
=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
108 elif [ -x "$(which ip 2>/dev/null)" ]; then
109 docker_ip
=`ip addr show docker0 | \
111 awk '{ split($2,a,"/"); print a[1] }';`
113 docker_ip
=`ifconfig | \
114 grep -B1 "inet addr" | \
115 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
117 awk -F: '{ print $3 }';`
120 # From https://stackoverflow.com/a/44660519/702738
122 if [[ $1 == $2 ]]; then
126 local i a
=(${1%%[^0-9.]*}) b
=(${2%%[^0-9.]*})
127 local arem
=${1#${1%%[^0-9.]*}} brem=${2#${2%%[^0-9.]*}}
128 for ((i
=0; i
<${#a[@]} || i
<${#b[@]}; i
++)); do
129 if ((10#${a[i]:-0} < 10#${b[i]:-0})); then
131 elif ((10#${a[i]:-0} > 10#${b[i]:-0})); then
135 if [ "$arem" '<' "$brem" ]; then
137 elif [ "$arem" '>' "$brem" ]; then
149 echo "Docker is not installed, you will need to install Docker in order to run Launcher"
150 echo "See https://docs.docker.com/installation/"
155 # Add a single retry to work around dockerhub TLS errors
156 $docker_path pull
$image ||
$docker_path pull
$image
161 if [ -z $docker_path ]; then
165 # 1. docker daemon running?
166 # we send stderr to /dev/null cause we don't care about warnings,
167 # it usually complains about swap which does not matter
168 test=`$docker_path info 2> /dev/null`
169 if [[ $?
-ne 0 ]] ; then
170 echo "Cannot connect to the docker daemon - verify it is running and you have access"
174 # 2. running an approved storage driver?
175 if ! $docker_path info
2> /dev
/null |
egrep -q 'Storage Driver: (btrfs|aufs|zfs|overlay2)$'; then
176 echo "Your Docker installation is not using a supported storage driver. If we were to proceed you may have a broken install."
177 echo "overlay2 is the recommended storage driver, although zfs and aufs may work as well."
178 echo "Other storage drivers are known to be problematic."
179 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the 'Storage Driver' line."
181 echo "If you wish to continue anyway using your existing unsupported storage driver,"
182 echo "read the source code of launcher and figure out how to bypass this check."
186 # 3. running recommended docker version
187 test=($
($docker_path --version)) # Get docker version string
188 test=${test[2]//,/} # Get version alone and strip comma if exists
190 # At least minimum docker version
191 if compare_version
"${docker_min_version}" "${test}"; then
192 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
196 # Recommend newer docker version
197 if compare_version
"${docker_rec_version}" "${test}"; then
198 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
204 echo "ERROR: 32bit arm is not supported. Check if your hardware support arm64, which is supported in experimental capacity."
208 echo "WARNING: Support for aarch64 is experimental at the moment. Please report any problems at https://meta.discourse.org/tag/arm "
209 image
="discourse/base:aarch64"
211 read -n 1 -s -r -p "Press any key to continue"
214 echo "x86_64 arch detected."
217 echo "ERROR: unknown arch detected."
223 # 4. discourse docker image is downloaded
224 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
226 # arm experimental support is on a fixed tag, always pull
227 if [ -z "$test" ] ||
[ $arm = true
]; then
229 echo "WARNING: We are about to start downloading the Discourse base image"
230 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
232 echo "Please be patient"
238 # 5. running recommended git version
239 test=($
($git_path --version)) # Get git version string
240 test=${test[2]//,/} # Get version alone and strip comma if exists
242 # At least minimum version
243 if compare_version
"${git_min_version}" "${test}"; then
244 echo "ERROR: Git version ${test} not supported, please upgrade to at least ${git_min_version}, or recommended ${git_rec_version}"
248 # Recommend best version
249 if compare_version
"${git_rec_version}" "${test}"; then
250 echo "WARNING: Git version ${test} deprecated, recommend upgrade to ${git_rec_version} or newer."
253 # 6. able to attach stderr / out / tty
254 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
255 if [[ "$test" =~
"working" ]] ; then : ; else
256 echo "Your Docker installation is not working correctly"
258 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
262 # 7. enough space for the bootstrap on docker folder
263 folder
=`$docker_path info --format '{{.DockerRootDir}}'`
264 safe_folder
=${folder:-/var/lib/docker}
265 if [[ -d $safe_folder && $
(stat
-f --format="%a*%S" $safe_folder)/1024**3 -lt 5 ]] ; then
266 echo "You have less than 5GB of free space on the disk where $safe_folder is located. You will need more space to continue"
269 if tty
>/dev
/null
; then
270 read -p "Would you like to attempt to recover space by cleaning docker images and containers in the system? (y/N)" -n 1 -r
272 if [[ $REPLY =~ ^
[Yy
]$
]]
274 $docker_path container prune
--force --filter until=24h
>/dev
/null
275 $docker_path image prune
--all --force --filter until=24h
>/dev
/null
276 echo "If the cleanup was successful, you may try again now"
282 # 8. container definition file is accessible and is not insecure (world-readable)
283 if [[ ! -e "$config_file" ||
! -r "$config_file" ]]; then
284 echo "ERROR: $config_file does not exist or is not readable."
286 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
288 elif [[ "$(find $config_file -perm -004)" ]]; then
289 echo "WARNING: $config_file file is world-readable. You can secure this file by running: chmod o-rwx $config_file"
294 if [ -z "$SKIP_PREREQS" ] && [ "$command" != "cleanup" ]; then
299 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
300 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
304 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
305 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
309 local templates
=`cat $1 | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
310 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
312 local arrTemplates
=${templates// / }
314 if [ ! -z "$templates" ]; then
321 set_template_info
() {
322 templates
=$
(find_templates
$config_file)
324 arrTemplates
=(${templates// / })
325 config_data
=$
(cat $config_file)
329 for template
in "${arrTemplates[@]}"
331 [ ! -z $template ] && {
332 input
="$input _FILE_SEPERATOR_ $(cat $template)"
336 # we always want our config file last so it takes priority
337 input
="$input _FILE_SEPERATOR_ $config_data"
339 read -r -d '' env_ruby
<< 'RUBY'
342 input
=STDIN.readlines.
join
343 # default to UTF-8 for the dbs sake
344 env
= {'LANG' => 'en_US.UTF-8'}
345 input.
split('_FILE_SEPERATOR_').each
do |yml|
348 env.merge
!(YAML.load
(yml
)['env'] ||
{})
349 rescue Psych
::SyntaxError
=> e
357 env.each
{|k
,v| puts
"*ERROR." if v.is_a?
(Hash
)}
358 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
361 tmp_input_file
=$
(mktemp
)
363 echo "$input" > "$tmp_input_file"
364 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
366 rm -f "$tmp_input_file"
371 if [ "$i" == "*ERROR." ]; then
373 elif [ -n "$i" ]; then
374 env
[${#env[@]}]="${i//\{\{config\}\}/${config}}"
378 if [ "$ok" -ne 1 ]; then
380 echo "YAML syntax error. Please check your containers
/*.yml config files.
"
385 read -r -d '' labels_ruby << 'RUBY'
388 input=STDIN.readlines.join
390 input.split('_FILE_SEPERATOR_').each do |yml|
393 labels.merge!(YAML.load(yml)['labels'] || {})
394 rescue Psych::SyntaxError => e
402 puts labels.map{|k,v| "-l\n#{k}=#{v}" }.join("\n")
405 tmp_input_file
=$
(mktemp
)
407 echo "$input" > "$tmp_input_file"
408 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$labels_ruby"`
410 rm -f "$tmp_input_file"
415 if [ "$i" == "*ERROR." ]; then
417 elif [ -n "$i" ]; then
418 labels
[${#labels[@]}]=$
(echo $i |
sed s
/{{config
}}/${config}/g
)
422 if [ "$ok" -ne 1 ]; then
424 echo "YAML syntax error. Please check your containers/*.yml config files."
429 read -r -d '' ports_ruby
<< 'RUBY'
432 input
=STDIN.readlines.
join
434 input.
split('_FILE_SEPERATOR_').each
do |yml|
437 ports
+= (YAML.load
(yml
)['expose'] ||
[])
438 rescue Psych
::SyntaxError
=> e
446 puts ports.map
{ |p| p.to_s.include?
(':') ?
"-p\n#{p}" : "--expose\n#{p}" }.
join("\n")
449 tmp_input_file
=$
(mktemp
)
451 echo "$input" > "$tmp_input_file"
452 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$ports_ruby"`
454 rm -f "$tmp_input_file"
459 if [ "$i" == "*ERROR." ]; then
461 elif [ -n "$i" ]; then
462 ports
[${#ports[@]}]=$i
466 if [ "$ok" -ne 1 ]; then
468 echo "YAML syntax error. Please check your containers/*.yml config files."
475 if [ -z $docker_path ]; then
479 [ "$command" == "cleanup" ] && {
480 $docker_path container prune
--filter until=1h
481 $docker_path image prune
--all --filter until=1h
483 if [ -d /var
/discourse
/shared
/standalone
/postgres_data_old
]; then
485 echo "Old PostgreSQL backup data cluster detected taking up $(du -hs /var/discourse/shared/standalone/postgres_data_old | awk '{print $1}') detected"
486 read -p "Would you like to remove it? (y/N): " -n 1 -r && echo
488 if [[ $REPLY =~ ^
[Yy
]$
]]; then
489 echo "removing old PostgreSQL data cluster at /var/discourse/shared/standalone/postgres_data_old..."
490 rm -rf /var
/discourse
/shared
/standalone
/postgres_data_old
*
499 docker_version
=($
($docker_path --version))
500 docker_version
=${test[2]//,/}
501 restart_policy
=${restart_policy:---restart=always}
503 set_existing_container
(){
504 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
509 set_existing_container
511 if [ ! -z $existing ]
515 $docker_path stop
-t 30 $config
518 echo "$config was not started !"
519 echo "./discourse-doctor may help diagnose the problem."
525 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
526 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
528 if [ -n "$user_run_image" ]; then
529 run_image
=$user_run_image
530 elif [ -z "$run_image" ]; then
531 run_image
="$local_discourse/$config"
536 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
537 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
539 if [ -z "$boot_command" ]; then
541 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
542 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
544 if [ -z "$no_boot_command" ]; then
545 boot_command
="/sbin/boot"
553 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
554 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
556 if [[ -n "$docker_args" ]]; then
557 user_args
="$user_args_argv $docker_args"
563 if [ -z "$START_CMD_ONLY" ]
565 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
567 if [ ! -z $existing ]
569 echo "Nothing to do, your container has already started!"
573 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
574 if [ ! -z $existing ]
576 echo "starting up existing container"
579 $docker_path start
$config
591 # get hostname and settings from container configuration
592 for envar
in "${env[@]}"
594 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
596 # use as environment variable
602 hostname
=`hostname -s`
604 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
606 hostname
=$DISCOURSE_HOSTNAME
608 hostname
=$hostname-$config
611 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
612 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
613 # docker added more hostname rules
614 hostname
=${hostname//_/-}
617 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
618 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/'")"
621 if [ ! -z "$START_CMD_ONLY" ] ; then
627 $docker_path run
--shm-size=512m
$links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
628 -e DOCKER_HOST_IP
="$docker_ip" --name $config -t "${ports[@]}" $volumes $mac_address $user_args \
629 $run_image $boot_command
643 (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 \
644 /bin
/bash
-c "$run_command") || ERR
=$?
646 if [[ $ERR > 0 ]]; then
654 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
655 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
657 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
658 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
660 if [[ ! X
"" = X
"$base_image" ]]; then
664 # the base_image may not always be discourse/base,
665 # let's ensure we always build from the latest
671 if $docker_path run
$user_args --rm -i $image gem
which pups
; then
672 pups_command
="/usr/local/bin/pups --stdin"
674 # Fallback to git pull method here if `pups` was not installed by gem in base image
675 pups_command
="cd /pups &&"
676 if [[ ! "false" = $update_pups ]]; then
677 pups_command
="$pups_command git pull && git checkout $pups_version &&"
679 pups_command
="$pups_command /pups/bin/pups --stdin"
684 declare -i BOOTSTRAP_EXITCODE
687 echo "$input" |
$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 \
688 /bin
/bash
-c "$pups_command"
689 BOOTSTRAP_EXITCODE
=$?
691 CONTAINER_ID
=$
(cat "$cidbootstrap")
692 rm -f "$cidbootstrap"
694 # magic exit code that indicates a retry
695 if [[ $BOOTSTRAP_EXITCODE -eq 77 ]]; then
696 $docker_path rm "$CONTAINER_ID"
698 elif [[ $BOOTSTRAP_EXITCODE -gt 0 ]]; then
699 echo "bootstrap failed with exit code $BOOTSTRAP_EXITCODE"
700 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one."
701 echo "./discourse-doctor may help diagnose the problem."
703 if [[ -n "$DEBUG" ]]; then
704 if $docker_path commit
"$CONTAINER_ID" $local_discourse/$config-debug; then
705 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
707 echo "** DEBUG ** Failed to commit container $CONTAINER_ID for diagnostics"
711 $docker_path rm "$CONTAINER_ID"
717 $docker_path commit \
718 -c "LABEL org.opencontainers.image.created=\"$(TZ=UTC date -Iseconds)\"" \
720 $local_discourse/$config || fatal
"FAILED TO COMMIT $CONTAINER_ID"
721 $docker_path rm "$CONTAINER_ID"
727 echo "Successfully bootstrapped, to startup use ./launcher start $config"
737 exec $docker_path exec -it $config /bin
/bash
--login
747 $docker_path logs
$config
769 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
770 git branch
-m master main
772 git branch
-u origin
/main main
773 git remote set-head origin
-a
776 if [ "$(git symbolic-ref --short HEAD)" == "main" ]; then
777 echo "Ensuring launcher is up to date"
781 LOCAL
=$
(git rev-parse HEAD
)
782 REMOTE
=$
(git rev-parse @
{u
})
783 BASE
=$
(git merge-base HEAD @
{u
})
785 if [ $LOCAL = $REMOTE ]; then
786 echo "Launcher is up-to-date"
788 elif [ $LOCAL = $BASE ]; then
789 echo "Updating Launcher..."
790 git pull ||
(echo 'failed to update' && exit 1)
792 echo "Launcher updated, restarting..."
793 exec "$0" "${SAVED_ARGV[@]}"
795 elif [ $REMOTE = $BASE ]; then
796 echo "Your version of Launcher is ahead of origin"
799 echo "Launcher has diverged source, this is only expected in Dev mode"
804 set_existing_container
806 if [ ! -z $existing ]
808 echo "Stopping old container"
811 $docker_path stop
-t 60 $config
817 if [ ! -z $existing ]
819 echo "Removing old container"
822 $docker_path rm $config
832 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)