7 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
8 re
='[A-Z/ !@#$%^&*()+~`=]'
9 if [[ $config =~
$re ]];
12 echo "ERROR: Config name must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
19 docker_min_version
='1.6.0'
20 docker_rec_version
='1.6.0'
22 config_file
=containers
/"$config".yml
23 cidbootstrap
=cids
/"$config"_bootstrap.cid
24 local_discourse
=local_discourse
25 image
=discourse
/discourse
:1.0.17
26 docker_path
=`which docker.io || which docker`
27 template_path
=samples
/standalone_template.yml
28 changelog
=/tmp
/changelog
# used to test whether sed did anything
30 if [ "${SUPERVISED}" = "true" ]; then
31 restart_policy
="--restart=no"
33 attach_on_run
="-a stdout -a stderr"
38 if [ -n "$DOCKER_HOST" ]; then
39 docker_ip
=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
40 elif [ -x "$(which ip 2>/dev/null)" ]; then
41 docker_ip
=`ip addr show docker0 | \
43 awk '{ split($2,a,"/"); print a[1] }';`
45 docker_ip
=`ifconfig | \
46 grep -B1 "inet addr" | \
47 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
49 awk -F: '{ print $3 }';`
54 echo "Usage: launcher COMMAND CONFIG [--skip-prereqs]"
56 echo " start: Start/initialize a container"
57 echo " stop: Stop a running container"
58 echo " restart: Restart a container"
59 echo " destroy: Stop and remove a container"
60 echo " enter: Use nsenter to enter a container"
61 echo " ssh: Start a bash shell in a running container"
62 echo " logs: Docker logs for container"
63 echo " bootstrap: Bootstrap a container for the config based on a template"
64 echo " rebuild: Rebuild a container (destroy old, bootstrap, start new)"
65 echo " memconfig: Configure defaults based on available RAM"
66 echo " cleanup: Remove all containers that have stopped for > 24 hours"
69 echo " --skip-prereqs Don't check prerequisites or resource requirements"
70 echo " --docker-args Extra arguments to pass when running docker"
77 IFS
=.
read -a ver_a
<<< "$1"
78 IFS
=.
read -a ver_b
<<< "$2"
80 while [[ -n $ver_a ]]; do
81 if (( ver_a
> ver_b
)); then
83 elif (( ver_b
> ver_a
)); then
92 return 1 # They are equal
97 # 1. docker daemon running?
98 # we send stderr to /dev/null cause we don't care about warnings,
99 # it usually complains about swap which does not matter
100 test=`$docker_path info 2> /dev/null`
102 if [[ $?
-ne 0 ]] ; then
103 echo "Cannot connect to the docker daemon - verify it is running and you have access"
107 # 2. running aufs or btrfs
108 test=`$docker_path info 2> /dev/null | grep 'Driver: '`
109 if [[ "$test" =~
[aufs|btrfs|zfs|overlay
] ]] ; then : ; else
110 echo "Your Docker installation is not using a supported filesystem if we were to proceed you may have a broken install."
111 echo "aufs is the recommended filesystem you should be using (zfs/btrfs and overlay may work as well)"
112 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the driver"
114 echo "If you wish to continue anyway using your existing unsupported filesystem"
115 echo "read the source code of launcher and figure out how to bypass this."
119 # 3. running recommended docker version
120 test=($
($docker_path --version)) # Get docker version string
121 test=${test[2]//,/} # Get version alone and strip comma if exists
123 # At least minimum version
124 if compare_version
"${docker_min_version}" "${test}"; then
125 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
129 # Recommend best version
130 if compare_version
"${docker_rec_version}" "${test}"; then
131 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
134 # 4. discourse docker image is downloaded
135 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
137 if [ -z "$test" ]; then
139 echo "WARNING: We are about to start downloading the Discourse base image"
140 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
142 echo "Please be patient"
147 # 5. able to attach stderr / out / tty
148 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
149 if [[ "$test" =~
"working" ]] ; then : ; else
150 echo "Your Docker installation is not working correctly"
152 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
161 avail_mem
="$(LANG=C free -m | grep '^Mem:' | awk '{print $2}')"
162 if [ "$avail_mem" -lt 900 ]; then
163 resources
="insufficient"
164 echo "WARNING: You do not appear to have sufficient memory to run Discourse."
166 echo "Your system may not work properly, or future upgrades of Discourse may"
167 echo "not complete successfully."
169 echo "See https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#create-new-cloud-server"
170 elif [ "$avail_mem" -lt 1800 ]; then
171 total_swap
="$(LANG=C free -m | grep ^Swap: | awk '{print $2}')"
172 if [ "$total_swap" -lt 1000 ]; then
173 resources
="insufficient"
174 echo "WARNING: You must have at least 1GB of swap when running with less"
175 echo "than 2GB of RAM."
177 echo "Your system may not work properly, or future upgrades of Discourse may"
178 echo "not complete successfully."
180 echo "See https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#set-up-swap-if-needed"
185 free_disk
="$(df /var | tail -n 1 | awk '{print $4}')"
186 if [ "$free_disk" -lt 5000 ]; then
187 resources
="insufficient"
188 echo "WARNING: You must have at least 5GB of *free* disk space to run Discourse."
190 echo "Insufficient disk space may result in problems running your site, and may"
191 echo "not even allow Discourse installation to complete successfully."
193 echo "Please free up some space, or expand your disk, before continuing."
197 if [ -t 0 ] && [ "$resources" != "ok" ]; then
199 read -p "Press ENTER to continue, or Ctrl-C to exit and give your system more resources"
204 local valid
=$
(netstat
-tln |
awk '{print $4}' |
grep ":${1}\$")
206 if [ -n "$valid" ]; then
207 echo "Launcher has detected that port ${1} is in use."
209 echo "If you are trying to run Discourse simultaneously with another web server like Apache or nginx, you will need to bind to a different port."
210 echo "See https://meta.discourse.org/t/17247 for help."
211 echo "To continue anyway, re-run Launcher with --skip-prereqs"
216 if [ "$opt" != "--skip-prereqs" ] ; then
220 if [ "$opt" == "--docker-args" ] ; then
227 local ${ssh_key_locations}
229 ~
/.ssh
/id_ed25519.pub
233 ~core
/.ssh
/authorized_keys
237 for keyfile
in "${ssh_key_locations[@]}"; do
238 if [[ -e ${keyfile} ]] ; then
239 ssh_pub_key
="$(cat ${keyfile})"
250 echo "Docker is not installed, you will need to install Docker in order to run Discourse"
251 echo "Please visit https://docs.docker.com/installation/ for instructions on how to do this for your system"
253 echo "If you are running Ubuntu Trusty or later, you can try the following:"
256 echo "sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D"
257 echo "sudo sh -c \"echo deb https://apt.dockerproject.org/repo ubuntu-$(lsb_release -sc) main > /etc/apt/sources.list.d/docker.list\""
258 echo "sudo apt-get update"
259 echo "sudo apt-get install docker-engine"
265 read -r -d '' env_ruby
<< 'RUBY'
268 input
= STDIN.readlines.
join
269 yaml
= YAML.load
(input
)
271 if host_run
= yaml
['host_run']
272 params
= yaml
['params'] ||
{}
273 host_run.each
do |run|
275 run
= run.gsub
("$#{k}", v
)
277 STDOUT.
write "#{run}--SEP--"
282 host_run
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
284 while [ "$host_run" ] ; do
285 iter
=${host_run%%--SEP--*}
287 echo "Host run: $iter"
290 host_run
="${host_run#*--SEP--}"
296 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
297 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
301 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
302 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
305 set_template_info
() {
307 templates
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
308 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
311 arrTemplates
=(${templates// / })
312 config_data
=$
(cat $config_file)
316 for template
in "${arrTemplates[@]}"
318 [ ! -z $template ] && {
319 input
="$input _FILE_SEPERATOR_ $(cat $template)"
323 # we always want our config file last so it takes priority
324 input
="$input _FILE_SEPERATOR_ $config_data"
326 read -r -d '' env_ruby
<< 'RUBY'
329 input
=STDIN.readlines.
join
330 # default to UTF-8 for the dbs sake
331 env
= {'LANG' => 'en_US.UTF-8'}
332 input.
split('_FILE_SEPERATOR_').each
do |yml|
335 env.merge
!(YAML.load
(yml
)['env'] ||
{})
336 rescue Psych
::SyntaxError
=> e
344 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
347 raw
=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
352 if [ "$i" == "*ERROR." ]; then
354 elif [ -n "$i" ]; then
359 if [ "$ok" -ne 1 ]; then
361 echo "YAML syntax error. Please check your /var/discourse/containers/*.yml config files."
366 [ -z $docker_path ] && {
370 [ "$command" == "cleanup" ] && {
372 echo "The following command will"
373 echo "- Delete all docker images for old containers"
374 echo "- Delete all stopped and orphan containers"
376 read -p "Are you sure (Y/n): " -n 1 -r && echo
377 if [[ $REPLY =~ ^
[Yy
]$ ||
! $REPLY ]]
379 space
=$
(df
/var
/lib
/docker |
awk '{ print $4 }' |
grep -v Available
)
380 echo "Starting Cleanup (bytes free $space)"
382 STATE_DIR
=.
/.gc-state
scripts
/docker-gc
384 space
=$
(df
/var
/lib
/docker |
awk '{ print $4 }' |
grep -v Available
)
385 echo "Finished Cleanup (bytes free $space)"
397 if [[ ! -e $config_file && $command -ne "memconfig" ]]
399 echo "Config file was not found, ensure $config_file exists"
401 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
405 docker_version
=($
($docker_path --version))
406 docker_version
=${test[2]//,/}
407 restart_policy
=${restart_policy:---restart=always}
409 set_existing_container
(){
410 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
415 set_existing_container
417 if [ ! -z $existing ]
421 $docker_path stop
-t 10 $config
424 echo "$config was not started !"
430 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
431 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
433 if [ -z "$run_image" ]; then
434 run_image
="$local_discourse/$config"
439 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
440 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
442 if [ -z "$boot_command" ]; then
444 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
445 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
447 if [ -z "$no_boot_command" ]; then
448 boot_command
="/sbin/boot"
454 if [ "$opt" != "--skip-prereqs" ] ; then
457 if [ -f $config_file ]
459 cp $config_file $config_file.bak
460 echo "Saving $config_file as $config_file.bak"
462 echo "Creating $config_file from $template_path"
463 if [ ! -f $template_path ]
465 echo "$template_path is missing. Exiting."
468 cp $template_path $config_file
472 avail_mem
="$(LANG=C free -m | grep '^Mem:' | awk '{print $2}')"
473 avail_gb
=`expr $(($avail_mem / 950))`
474 avail_cores
=`grep -c processor /proc/cpuinfo`
475 echo "Found $avail_mem (${avail_gb}GB), of memory and $avail_cores cores."
477 # set db_shared_buffers: "128MB" (1GB) or 256MB * GB
478 if [ "$avail_gb" -eq "1" ]
480 db_shared_buffers
="128"
482 db_shared_buffers
=`expr $avail_gb \* 256`
484 echo -e "Setting db_shared_buffers to ${db_shared_buffers}GB\c"
485 sed -i -e "s/^ db_shared_buffers:.*/ db_shared_buffers: \"${db_shared_buffers}GB\"/w $changelog" $config_file
488 echo " successfully."
491 echo -e ". . . oops!\n---> db_shared_buffers not found in $config_file. Retaining defaults."
494 # set UNICORN_WORKERS: 2*GB or 2*cores (the same on DO)
495 if [ "$avail_gb" -le "2" ]
497 unicorn_workers
=`expr $avail_gb \* 2`
499 unicorn_workers
=`expr $avail_cores \* 2`
502 echo -e "Setting UNICORN_WORKERS to $unicorn_workers\c"
503 sed -i -e "s/^ UNICORN_WORKERS:.*/ UNICORN_WORKERS: ${unicorn_workers}/w $changelog" $config_file
506 echo " successfully."
509 echo -e ". . . oops!\n---> UNICORN_WORKERS not found in $config_file. Retaining defaults.\n"
515 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
517 if [ ! -z $existing ]
519 echo "Nothing to do, your container has already started!"
523 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
524 if [ ! -z $existing ]
526 echo "starting up existing container"
529 $docker_path start
$config
535 ports
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
536 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['expose'].map{|p| \"-p #{p}\"}.join(' ')"`
538 IFS
='-p ' read -a array
<<< "$ports"
539 for element
in "${array[@]}"
541 IFS
=':' read -a args
<<< "$element"
542 if [ "${#args[@]}" == "2" ]; then
543 check_ports
"${args[0]}"
544 elif [ "${#args[@]}" == "3" ]; then
545 check_ports
"${args[1]}"
549 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
550 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
558 # get hostname and settings from container configuration
559 for envar
in "${env[@]}"
561 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
563 # use as environment variable
569 hostname
=`hostname -s`
571 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
573 hostname
=$DISCOURSE_HOSTNAME
575 hostname
=$hostname-$config
578 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
579 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
580 # docker added more hostname rules
581 hostname
=${hostname/_/-}
584 $docker_path run
$user_args $links $attach_on_run $restart_policy "${env[@]}" -h "$hostname" \
585 -e DOCKER_HOST_IP
=$docker_ip --name $config -t $ports $volumes $docker_args $run_image $boot_command
593 mail_config_verbose
=0 # 1 prints mail config to stdout
595 for x
in DISCOURSE_SMTP_ADDRESS DISCOURSE_SMTP_USER_NAME DISCOURSE_SMTP_PASSWORD
597 mail_var
=`grep "^ $x:" $config_file`
601 if [ "$mail_config_verbose" -eq 1 ]; then
605 echo "Warning: $x not configured."
606 mail_config
="dubious"
609 if [ -t 0 ] && [ "$mail_config" != "ok" ]; then
611 read -p "Press ENTER to continue, or Ctrl-C to exit and fix your mail config."
617 if [ "$opt" != "--skip-prereqs" ] ; then
627 # Is the image available?
628 # If not, pull it here so the user is aware what's happening.
629 $docker_path history $image >/dev
/null
2>&1 ||
$docker_path pull
$image
633 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
634 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
636 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
637 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
639 if [[ ! X
"" = X
"$base_image" ]]; then
648 run_command
="cd /pups &&"
649 if [[ ! "false" = $update_pups ]]; then
650 run_command
="$run_command git pull &&"
652 run_command
="$run_command /pups/bin/pups --stdin"
656 env
=("${env[@]}" "-e" "SSH_PUB_KEY=$ssh_pub_key")
658 (exec echo "$input" |
$docker_path run
$user_args $links "${env[@]}" -e DOCKER_HOST_IP
=$docker_ip --cidfile $cidbootstrap -i -a stdin
-a stdout
-a stderr
$volumes $image \
659 /bin
/bash
-c "$run_command") \
660 ||
($docker_path rm `cat $cidbootstrap` && rm $cidbootstrap)
662 [ ! -e $cidbootstrap ] && echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one" && exit 1
666 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
667 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
675 echo "Successfully bootstrapped, to startup use ./launcher start $config"
680 exec $docker_path exec -it $config /bin
/bash
--login
684 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
686 if [[ ! -z $existing ]]; then
687 address
="`$docker_path port $config 22`"
688 split=(${address//:/ })
689 exec ssh -o StrictHostKeyChecking
=no root@
${split[0]} -p ${split[1]}
691 echo "$config is not running!"
703 $docker_path logs
$config
724 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
725 echo "Ensuring discourse docker is up to date"
729 LOCAL
=$
(git rev-parse @
)
730 REMOTE
=$
(git rev-parse @
{u
})
731 BASE
=$
(git merge-base @ @
{u
})
733 if [ $LOCAL = $REMOTE ]; then
734 echo "Discourse Docker is up-to-date"
736 elif [ $LOCAL = $BASE ]; then
737 echo "Updating Discourse Docker"
738 git pull ||
(echo 'failed to update' && exit 1)
741 elif [ $REMOTE = $BASE ]; then
742 echo "Your version of Discourse Docker is ahead of origin"
745 echo "Discourse Docker has diverged source, this is only expected in Dev mode"
750 set_existing_container
752 if [ ! -z $existing ]
754 echo "Stopping old container"
757 $docker_path stop
-t 10 $config
763 if [ ! -z $existing ]
765 echo "Removing old container"
768 $docker_path rm $config
778 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)