4 echo "Usage: launcher COMMAND CONFIG [--skip-prereqs] [--skip-discourse-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: Use nsenter to enter a container"
11 echo " logs: Docker logs for container"
12 echo " bootstrap: Bootstrap a container for the config based on a template"
13 echo " rebuild: Rebuild a container (destroy old, bootstrap, start new)"
14 echo " cleanup: Remove all containers that have stopped for > 24 hours"
17 echo " --skip-prereqs Don't check launcher prerequisites"
18 echo " --skip-discourse-prereqs Don't check prerequisites specifiy to Discourse"
19 echo " --docker-args Extra arguments to pass when running docker"
32 while [ ${#} -gt 0 ]; do
38 --skip-discourse-prereqs)
39 SKIP_DISCOURSE_PREREQS
="1"
46 echo "Unknown options '${1}'"
54 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
55 re
='[A-Z/ !@#$%^&*()+~`=]'
56 if [[ $config =~
$re ]];
59 echo "ERROR: Config name must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
66 docker_min_version
='1.6.0'
67 docker_rec_version
='1.6.0'
68 git_min_version
='1.8.0'
69 git_rec_version
='1.8.0'
71 config_file
=containers
/"$config".yml
72 cidbootstrap
=cids
/"$config"_bootstrap.cid
73 local_discourse
=local_discourse
74 image
=discourse
/discourse
:1.0.17
75 docker_path
=`which docker.io || which docker`
77 template_path
=samples
/standalone.yml
78 changelog
=/tmp
/changelog
# used to test whether sed did anything
80 if [ "${SUPERVISED}" = "true" ]; then
81 restart_policy
="--restart=no"
83 attach_on_run
="-a stdout -a stderr"
88 if [ -n "$DOCKER_HOST" ]; then
89 docker_ip
=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
90 elif [ -x "$(which ip 2>/dev/null)" ]; then
91 docker_ip
=`ip addr show docker0 | \
93 awk '{ split($2,a,"/"); print a[1] }';`
95 docker_ip
=`ifconfig | \
96 grep -B1 "inet addr" | \
97 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
99 awk -F: '{ print $3 }';`
105 IFS
=.
read -a ver_a
<<< "$1"
106 IFS
=.
read -a ver_b
<<< "$2"
108 while [[ -n $ver_a ]]; do
109 if (( ver_a
> ver_b
)); then
111 elif (( ver_b
> ver_a
)); then
115 ver_a
=("${ver_a[@]}")
117 ver_b
=("${ver_b[@]}")
120 return 1 # They are equal
126 echo "Docker is not installed, you will need to install Docker in order to run Discourse"
127 echo "Please visit https://docs.docker.com/installation/ for instructions on how to do this for your system"
129 echo "If you are running a recent Ubuntu Server, try the following:"
130 echo "sudo apt-get install docker-engine"
137 if [ -z $docker_path ]; then
141 # 1. docker daemon running?
142 # we send stderr to /dev/null cause we don't care about warnings,
143 # it usually complains about swap which does not matter
144 test=`$docker_path info 2> /dev/null`
145 if [[ $?
-ne 0 ]] ; then
146 echo "Cannot connect to the docker daemon - verify it is running and you have access"
150 # 2. running aufs or btrfs
151 test=`$docker_path info 2> /dev/null | grep 'Driver: '`
152 if [[ "$test" =~
[aufs|btrfs|zfs|overlay
] ]] ; then : ; else
153 echo "Your Docker installation is not using a supported filesystem if we were to proceed you may have a broken install."
154 echo "aufs is the recommended filesystem you should be using (zfs/btrfs and overlay may work as well)"
155 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the driver"
157 echo "If you wish to continue anyway using your existing unsupported filesystem, "
158 echo "read the source code of launcher and figure out how to bypass this."
162 # 3. running recommended docker version
163 test=($
($docker_path --version)) # Get docker version string
164 test=${test[2]//,/} # Get version alone and strip comma if exists
166 # At least minimum version
167 if compare_version
"${docker_min_version}" "${test}"; then
168 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
172 # Recommend best version
173 if compare_version
"${docker_rec_version}" "${test}"; then
174 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
177 # 4. discourse docker image is downloaded
178 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
180 if [ -z "$test" ]; then
182 echo "WARNING: We are about to start downloading the Discourse base image"
183 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
185 echo "Please be patient"
190 # 5. running recommended git version
191 test=($
($git_path --version)) # Get git version string
192 test=${test[2]//,/} # Get version alone and strip comma if exists
194 # At least minimum version
195 if compare_version
"${git_min_version}" "${test}"; then
196 echo "ERROR: Git version ${test} not supported, please upgrade to at least ${git_min_version}, or recommended ${git_rec_version}"
200 # Recommend best version
201 if compare_version
"${git_rec_version}" "${test}"; then
202 echo "WARNING: Git version ${test} deprecated, recommend upgrade to ${git_rec_version} or newer."
205 # 6. able to attach stderr / out / tty
206 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
207 if [[ "$test" =~
"working" ]] ; then : ; else
208 echo "Your Docker installation is not working correctly"
210 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
221 avail_mem
="$(LANG=C free -m | grep '^Mem:' | awk '{print $2}')"
222 if [ "$avail_mem" -lt 900 ]; then
223 resources
="insufficient"
224 echo "WARNING: You do not appear to have sufficient memory to run Discourse."
226 echo "Your system may not work properly, or future upgrades of Discourse may"
227 echo "not complete successfully."
229 echo "See https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#create-new-cloud-server"
230 elif [ "$avail_mem" -lt 1800 ]; then
231 total_swap
="$(LANG=C free -m | grep ^Swap: | awk '{print $2}')"
232 if [ "$total_swap" -lt 1000 ]; then
233 resources
="insufficient"
234 echo "WARNING: You must have at least 1GB of swap when running with less"
235 echo "than 2GB of RAM."
237 echo "Your system may not work properly, or future upgrades of Discourse may"
238 echo "not complete successfully."
240 echo "See https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#set-up-swap-if-needed"
245 free_disk
="$(df /var | tail -n 1 | awk '{print $4}')"
246 if [ "$free_disk" -lt 5000 ]; then
247 resources
="insufficient"
248 echo "WARNING: You must have at least 5GB of *free* disk space to run Discourse."
250 echo "Insufficient disk space may result in problems running your site, and may"
251 echo "not even allow Discourse installation to complete successfully."
253 echo "Please free up some space, or expand your disk, before continuing."
255 echo "Run \`apt-get autoremove && apt-get autoclean\` to clean up unused packages and \`./launcher cleanup\` to remove stale Docker containers."
259 if [ -t 0 ] && [ "$resources" != "ok" ]; then
261 read -p "Press ENTER to continue, or Ctrl-C to exit and give your system more resources"
266 local valid
=$
(netstat
-tln |
awk '{print $4}' |
grep ":${1}\$")
268 if [ -n "$valid" ]; then
269 echo "Launcher has detected that port ${1} is in use."
271 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."
272 echo "See https://meta.discourse.org/t/17247 for help."
273 echo "To continue anyway, re-run Launcher with --skip-prereqs"
278 if [ -z "$SKIP_PREREQS" ] ; then
283 read -r -d '' env_ruby
<< 'RUBY'
286 input
= STDIN.readlines.
join
287 yaml
= YAML.load
(input
)
289 if host_run
= yaml
['host_run']
290 params
= yaml
['params'] ||
{}
291 host_run.each
do |run|
293 run
= run.gsub
("$#{k}", v
)
295 STDOUT.
write "#{run}--SEP--"
300 host_run
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
302 while [ "$host_run" ] ; do
303 iter
=${host_run%%--SEP--*}
305 echo "Host run: $iter"
308 host_run
="${host_run#*--SEP--}"
314 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
315 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
319 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
320 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
323 set_template_info
() {
325 templates
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
326 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
328 arrTemplates
=(${templates// / })
329 config_data
=$
(cat $config_file)
333 for template
in "${arrTemplates[@]}"
335 [ ! -z $template ] && {
336 input
="$input _FILE_SEPERATOR_ $(cat $template)"
340 # we always want our config file last so it takes priority
341 input
="$input _FILE_SEPERATOR_ $config_data"
343 read -r -d '' env_ruby
<< 'RUBY'
346 input
=STDIN.readlines.
join
347 # default to UTF-8 for the dbs sake
348 env
= {'LANG' => 'en_US.UTF-8'}
349 input.
split('_FILE_SEPERATOR_').each
do |yml|
352 env.merge
!(YAML.load
(yml
)['env'] ||
{})
353 rescue Psych
::SyntaxError
=> e
361 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
364 raw
=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
369 if [ "$i" == "*ERROR." ]; then
371 elif [ -n "$i" ]; then
376 if [ "$ok" -ne 1 ]; then
378 echo "YAML syntax error. Please check your /var/discourse/containers/*.yml config files."
383 if [ -z $docker_path ]; then
387 [ "$command" == "cleanup" ] && {
389 echo "The following command will"
390 echo "- Delete all docker images for old containers"
391 echo "- Delete all stopped and orphan containers"
393 read -p "Are you sure (Y/n): " -n 1 -r && echo
394 if [[ $REPLY =~ ^
[Yy
]$ ||
! $REPLY ]]
396 space
=$
(df
/var
/lib
/docker |
awk '{ print $4 }' |
grep -v Available
)
397 echo "Starting Cleanup (bytes free $space)"
399 STATE_DIR
=.
/.gc-state
scripts
/docker-gc
401 space
=$
(df
/var
/lib
/docker |
awk '{ print $4 }' |
grep -v Available
)
402 echo "Finished Cleanup (bytes free $space)"
410 if [[ ! -e $config_file ]]
412 echo "Config file was not found, ensure $config_file exists"
414 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
418 docker_version
=($
($docker_path --version))
419 docker_version
=${test[2]//,/}
420 restart_policy
=${restart_policy:---restart=always}
422 set_existing_container
(){
423 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
428 set_existing_container
430 if [ ! -z $existing ]
434 $docker_path stop
-t 10 $config
437 echo "$config was not started !"
443 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
444 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
446 if [ -z "$run_image" ]; then
447 run_image
="$local_discourse/$config"
452 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
453 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
455 if [ -z "$boot_command" ]; then
457 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
458 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
460 if [ -z "$no_boot_command" ]; then
461 boot_command
="/sbin/boot"
466 scale_ram_and_cpu
() {
468 # grab info about total system ram and physical (NOT LOGICAL!) CPU cores
469 avail_mem
="$(LANG=C free -m | grep '^Mem:' | awk '{print $2}')"
470 avail_gb
=$
(( $avail_mem / 950 ))
471 avail_cores
=`cat /proc/cpuinfo | grep "cpu cores" | uniq | awk '{print $4}'`
472 echo "Found ${avail_gb}GB of memory and $avail_cores physical CPU cores"
474 # db_shared_buffers: 128MB for 1GB, 256MB for 2GB, or 256MB * GB, max 4096MB
475 if [ "$avail_gb" -eq "1" ]
477 db_shared_buffers
=128
479 if [ "$avail_gb" -eq "2" ]
481 db_shared_buffers
=256
483 db_shared_buffers
=$
(( 256 * $avail_gb ))
486 db_shared_buffers
=$
(( db_shared_buffers
< 4096 ? db_shared_buffers
: 4096 ))
488 sed -i -e "s/^ #db_shared_buffers:.*/ db_shared_buffers: \"${db_shared_buffers}MB\"/w $changelog" $config_file
491 echo "setting db_shared_buffers = ${db_shared_buffers}MB based on detected CPU/RAM"
496 # UNICORN_WORKERS: 2 * GB for 2GB or less, or 2 * CPU, max 8
497 if [ "$avail_gb" -le "2" ]
499 unicorn_workers
=$
(( 2 * $avail_gb ))
501 unicorn_workers
=$
(( 2 * $avail_cores ))
503 unicorn_workers
=$
(( unicorn_workers
< 8 ? unicorn_workers
: 8 ))
505 sed -i -e "s/^ #UNICORN_WORKERS:.*/ UNICORN_WORKERS: ${unicorn_workers}/w $changelog" $config_file
508 echo "setting UNICORN_WORKERS = ${unicorn_workers} based on detected CPU/RAM"
516 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
518 if [ ! -z $existing ]
520 echo "Nothing to do, your container has already started!"
524 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
525 if [ ! -z $existing ]
527 echo "starting up existing container"
530 $docker_path start
$config
537 if [ -z "$SKIP_DISCOURSE_PREREQS" ] ; then
538 ports
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
539 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['expose'].map{|p| \"-p #{p}\"}.join(' ')"`
541 IFS
='-p ' read -a array
<<< "$ports"
542 for element
in "${array[@]}"
544 IFS
=':' read -a args
<<< "$element"
545 if [ "${#args[@]}" == "2" ]; then
546 check_ports
"${args[0]}"
547 elif [ "${#args[@]}" == "3" ]; then
548 check_ports
"${args[1]}"
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']"`
562 # get hostname and settings from container configuration
563 for envar
in "${env[@]}"
565 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
567 # use as environment variable
573 hostname
=`hostname -s`
575 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
577 hostname
=$DISCOURSE_HOSTNAME
579 hostname
=$hostname-$config
582 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
583 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
584 # docker added more hostname rules
585 hostname
=${hostname/_/-}
588 $docker_path run
$user_args $links $attach_on_run $restart_policy "${env[@]}" -h "$hostname" \
589 -e DOCKER_HOST_IP
=$docker_ip --name $config -t $ports $volumes $docker_args $run_image $boot_command
596 valid_config_check
() {
599 for x
in DISCOURSE_SMTP_ADDRESS DISCOURSE_SMTP_USER_NAME DISCOURSE_SMTP_PASSWORD \
600 DISCOURSE_DEVELOPER_EMAILS DISCOURSE_HOSTNAME
602 mail_var
=`grep "^ $x:" $config_file`
604 local default
="example.com"
607 if [[ $mail_var = *"$default"* ]]
609 echo "Warning: $x left at incorrect default of example.com"
613 echo "Warning: $x not configured"
617 if [ -t 0 ] && [ "$valid_config" != "y" ]; then
619 read -p "Press Ctrl-C to exit and edit $config_file or ENTER to continue"
624 if [ -z "$SKIP_DISCOURSE_PREREQS" ] ; then
625 # Does your system meet the minimum requirements?
628 # is our configuration file valid?
631 # make minor scaling adjustments for RAM and CPU
635 # I got no frigging clue what this does, ask Sam Saffron. It RUNS STUFF ON THE HOST I GUESS?
638 # Is the image available?
639 # If not, pull it here so the user is aware what's happening.
640 $docker_path history $image >/dev
/null
2>&1 ||
$docker_path pull
$image
644 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
645 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
647 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
648 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
650 if [[ ! X
"" = X
"$base_image" ]]; then
659 run_command
="cd /pups &&"
660 if [[ ! "false" = $update_pups ]]; then
661 run_command
="$run_command git pull &&"
663 run_command
="$run_command /pups/bin/pups --stdin"
667 (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 \
668 /bin
/bash
-c "$run_command") \
669 ||
($docker_path rm `cat $cidbootstrap` && rm $cidbootstrap)
671 [ ! -e $cidbootstrap ] && echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one" && exit 1
675 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
676 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
684 echo "Successfully bootstrapped, to startup use ./launcher start $config"
689 exec $docker_path exec -it $config /bin
/bash
--login
699 $docker_path logs
$config
715 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
716 echo "Ensuring discourse docker is up to date"
720 LOCAL
=$
(git rev-parse @
)
721 REMOTE
=$
(git rev-parse @
{u
})
722 BASE
=$
(git merge-base @ @
{u
})
724 if [ $LOCAL = $REMOTE ]; then
725 echo "Discourse Docker is up-to-date"
727 elif [ $LOCAL = $BASE ]; then
728 echo "Updating Discourse Docker"
729 git pull ||
(echo 'failed to update' && exit 1)
732 elif [ $REMOTE = $BASE ]; then
733 echo "Your version of Discourse Docker is ahead of origin"
736 echo "Discourse Docker has diverged source, this is only expected in Dev mode"
741 set_existing_container
743 if [ ! -z $existing ]
745 echo "Stopping old container"
748 $docker_path stop
-t 10 $config
754 if [ ! -z $existing ]
756 echo "Removing old container"
759 $docker_path rm $config
769 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)