4 echo "Usage: launcher COMMAND CONFIG [--skip-prereqs]"
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 prerequisites or resource requirements"
18 echo " --docker-args Extra arguments to pass when running docker"
26 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
27 re
='[A-Z/ !@#$%^&*()+~`=]'
28 if [[ $config =~
$re ]];
31 echo "ERROR: Config name must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
38 docker_min_version
='1.6.0'
39 docker_rec_version
='1.6.0'
40 git_min_version
='1.8.0'
41 git_rec_version
='1.8.0'
43 config_file
=containers
/"$config".yml
44 cidbootstrap
=cids
/"$config"_bootstrap.cid
45 local_discourse
=local_discourse
46 image
=discourse
/discourse
:1.0.17
47 docker_path
=`which docker.io || which docker`
49 template_path
=samples
/standalone.yml
50 changelog
=/tmp
/changelog
# used to test whether sed did anything
52 if [ "${SUPERVISED}" = "true" ]; then
53 restart_policy
="--restart=no"
55 attach_on_run
="-a stdout -a stderr"
60 if [ -n "$DOCKER_HOST" ]; then
61 docker_ip
=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
62 elif [ -x "$(which ip 2>/dev/null)" ]; then
63 docker_ip
=`ip addr show docker0 | \
65 awk '{ split($2,a,"/"); print a[1] }';`
67 docker_ip
=`ifconfig | \
68 grep -B1 "inet addr" | \
69 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
71 awk -F: '{ print $3 }';`
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
98 echo "Docker is not installed, you will need to install Docker in order to run Discourse"
99 echo "Please visit https://docs.docker.com/installation/ for instructions on how to do this for your system"
101 echo "If you are running a recent Ubuntu Server, try the following:"
102 echo "sudo apt-get install docker-engine"
109 if [ -z $docker_path ]; then
113 # 1. docker daemon running?
114 # we send stderr to /dev/null cause we don't care about warnings,
115 # it usually complains about swap which does not matter
116 test=`$docker_path info 2> /dev/null`
117 if [[ $?
-ne 0 ]] ; then
118 echo "Cannot connect to the docker daemon - verify it is running and you have access"
122 # 2. running aufs or btrfs
123 test=`$docker_path info 2> /dev/null | grep 'Driver: '`
124 if [[ "$test" =~
[aufs|btrfs|zfs|overlay
] ]] ; then : ; else
125 echo "Your Docker installation is not using a supported filesystem if we were to proceed you may have a broken install."
126 echo "aufs is the recommended filesystem you should be using (zfs/btrfs and overlay may work as well)"
127 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the driver"
129 echo "If you wish to continue anyway using your existing unsupported filesystem, "
130 echo "read the source code of launcher and figure out how to bypass this."
134 # 3. running recommended docker version
135 test=($
($docker_path --version)) # Get docker version string
136 test=${test[2]//,/} # Get version alone and strip comma if exists
138 # At least minimum version
139 if compare_version
"${docker_min_version}" "${test}"; then
140 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
144 # Recommend best version
145 if compare_version
"${docker_rec_version}" "${test}"; then
146 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
149 # 4. discourse docker image is downloaded
150 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
152 if [ -z "$test" ]; then
154 echo "WARNING: We are about to start downloading the Discourse base image"
155 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
157 echo "Please be patient"
162 # 5. running recommended git version
163 test=($
($git_path --version)) # Get git version string
164 test=${test[2]//,/} # Get version alone and strip comma if exists
166 # At least minimum version
167 if compare_version
"${git_min_version}" "${test}"; then
168 echo "ERROR: Git version ${test} not supported, please upgrade to at least ${git_min_version}, or recommended ${git_rec_version}"
172 # Recommend best version
173 if compare_version
"${git_rec_version}" "${test}"; then
174 echo "WARNING: Git version ${test} deprecated, recommend upgrade to ${git_rec_version} or newer."
177 # 6. able to attach stderr / out / tty
178 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
179 if [[ "$test" =~
"working" ]] ; then : ; else
180 echo "Your Docker installation is not working correctly"
182 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
193 avail_mem
="$(LANG=C free -m | grep '^Mem:' | awk '{print $2}')"
194 if [ "$avail_mem" -lt 900 ]; then
195 resources
="insufficient"
196 echo "WARNING: You do not appear to have sufficient memory to run Discourse."
198 echo "Your system may not work properly, or future upgrades of Discourse may"
199 echo "not complete successfully."
201 echo "See https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#create-new-cloud-server"
202 elif [ "$avail_mem" -lt 1800 ]; then
203 total_swap
="$(LANG=C free -m | grep ^Swap: | awk '{print $2}')"
204 if [ "$total_swap" -lt 1000 ]; then
205 resources
="insufficient"
206 echo "WARNING: You must have at least 1GB of swap when running with less"
207 echo "than 2GB of RAM."
209 echo "Your system may not work properly, or future upgrades of Discourse may"
210 echo "not complete successfully."
212 echo "See https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#set-up-swap-if-needed"
217 free_disk
="$(df /var | tail -n 1 | awk '{print $4}')"
218 if [ "$free_disk" -lt 5000 ]; then
219 resources
="insufficient"
220 echo "WARNING: You must have at least 5GB of *free* disk space to run Discourse."
222 echo "Insufficient disk space may result in problems running your site, and may"
223 echo "not even allow Discourse installation to complete successfully."
225 echo "Please free up some space, or expand your disk, before continuing."
227 echo "Run \`apt-get autoremove && apt-get autoclean\` to clean up unused packages and \`./launcher cleanup\` to remove stale Docker containers."
231 if [ -t 0 ] && [ "$resources" != "ok" ]; then
233 read -p "Press ENTER to continue, or Ctrl-C to exit and give your system more resources"
238 local valid
=$
(netstat
-tln |
awk '{print $4}' |
grep ":${1}\$")
240 if [ -n "$valid" ]; then
241 echo "Launcher has detected that port ${1} is in use."
243 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."
244 echo "See https://meta.discourse.org/t/17247 for help."
245 echo "To continue anyway, re-run Launcher with --skip-prereqs"
250 if [ "$opt" != "--skip-prereqs" ] ; then
254 if [ "$opt" == "--docker-args" ] ; then
261 read -r -d '' env_ruby
<< 'RUBY'
264 input
= STDIN.readlines.
join
265 yaml
= YAML.load
(input
)
267 if host_run
= yaml
['host_run']
268 params
= yaml
['params'] ||
{}
269 host_run.each
do |run|
271 run
= run.gsub
("$#{k}", v
)
273 STDOUT.
write "#{run}--SEP--"
278 host_run
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
280 while [ "$host_run" ] ; do
281 iter
=${host_run%%--SEP--*}
283 echo "Host run: $iter"
286 host_run
="${host_run#*--SEP--}"
292 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
293 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
297 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
298 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
301 set_template_info
() {
303 templates
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
304 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
306 arrTemplates
=(${templates// / })
307 config_data
=$
(cat $config_file)
311 for template
in "${arrTemplates[@]}"
313 [ ! -z $template ] && {
314 input
="$input _FILE_SEPERATOR_ $(cat $template)"
318 # we always want our config file last so it takes priority
319 input
="$input _FILE_SEPERATOR_ $config_data"
321 read -r -d '' env_ruby
<< 'RUBY'
324 input
=STDIN.readlines.
join
325 # default to UTF-8 for the dbs sake
326 env
= {'LANG' => 'en_US.UTF-8'}
327 input.
split('_FILE_SEPERATOR_').each
do |yml|
330 env.merge
!(YAML.load
(yml
)['env'] ||
{})
331 rescue Psych
::SyntaxError
=> e
339 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
342 raw
=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
347 if [ "$i" == "*ERROR." ]; then
349 elif [ -n "$i" ]; then
354 if [ "$ok" -ne 1 ]; then
356 echo "YAML syntax error. Please check your /var/discourse/containers/*.yml config files."
361 if [ -z $docker_path ]; then
365 [ "$command" == "cleanup" ] && {
367 echo "The following command will"
368 echo "- Delete all docker images for old containers"
369 echo "- Delete all stopped and orphan containers"
371 read -p "Are you sure (Y/n): " -n 1 -r && echo
372 if [[ $REPLY =~ ^
[Yy
]$ ||
! $REPLY ]]
374 space
=$
(df
/var
/lib
/docker |
awk '{ print $4 }' |
grep -v Available
)
375 echo "Starting Cleanup (bytes free $space)"
377 STATE_DIR
=.
/.gc-state
scripts
/docker-gc
379 space
=$
(df
/var
/lib
/docker |
awk '{ print $4 }' |
grep -v Available
)
380 echo "Finished Cleanup (bytes free $space)"
392 if [[ ! -e $config_file && $command -ne "memconfig" ]]
394 echo "Config file was not found, ensure $config_file exists"
396 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
400 docker_version
=($
($docker_path --version))
401 docker_version
=${test[2]//,/}
402 restart_policy
=${restart_policy:---restart=always}
404 set_existing_container
(){
405 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
410 set_existing_container
412 if [ ! -z $existing ]
416 $docker_path stop
-t 10 $config
419 echo "$config was not started !"
425 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
426 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
428 if [ -z "$run_image" ]; then
429 run_image
="$local_discourse/$config"
434 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
435 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
437 if [ -z "$boot_command" ]; then
439 no_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)['no_boot_command']"`
442 if [ -z "$no_boot_command" ]; then
443 boot_command
="/sbin/boot"
448 scale_ram_and_cpu
() {
450 # grab info about total system ram and physical (NOT LOGICAL!) CPU cores
451 avail_mem
="$(LANG=C free -m | grep '^Mem:' | awk '{print $2}')"
452 avail_gb
=$
(( $avail_mem / 950 ))
453 avail_cores
=`cat /proc/cpuinfo | grep "cpu cores" | uniq | awk '{print $4}'`
454 echo "Found ${avail_gb}GB of memory and $avail_cores physical CPU cores"
456 # db_shared_buffers: 128MB for 1GB, 256MB for 2GB, or 256MB * GB, max 4096MB
457 if [ "$avail_gb" -eq "1" ]
459 db_shared_buffers
=128
461 if [ "$avail_gb" -eq "2" ]
463 db_shared_buffers
=256
465 db_shared_buffers
=$
(( 256 * $avail_gb ))
468 db_shared_buffers
=$
(( db_shared_buffers
< 4096 ? db_shared_buffers
: 4096 ))
470 sed -i -e "s/^ #db_shared_buffers:.*/ db_shared_buffers: \"${db_shared_buffers}MB\"/w $changelog" $config_file
473 echo "setting db_shared_buffers = ${db_shared_buffers}MB based on detected CPU/RAM"
478 # UNICORN_WORKERS: 2 * GB for 2GB or less, or 2 * CPU, max 8
479 if [ "$avail_gb" -le "2" ]
481 unicorn_workers
=$
(( 2 * $avail_gb ))
483 unicorn_workers
=$
(( 2 * $avail_cores ))
485 unicorn_workers
=$
(( unicorn_workers
< 8 ? unicorn_workers
: 8 ))
487 sed -i -e "s/^ #UNICORN_WORKERS:.*/ UNICORN_WORKERS: ${unicorn_workers}/w $changelog" $config_file
490 echo "setting UNICORN_WORKERS = ${unicorn_workers} based on detected CPU/RAM"
498 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
500 if [ ! -z $existing ]
502 echo "Nothing to do, your container has already started!"
506 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
507 if [ ! -z $existing ]
509 echo "starting up existing container"
512 $docker_path start
$config
518 ports
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
519 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['expose'].map{|p| \"-p #{p}\"}.join(' ')"`
521 IFS
='-p ' read -a array
<<< "$ports"
522 for element
in "${array[@]}"
524 IFS
=':' read -a args
<<< "$element"
525 if [ "${#args[@]}" == "2" ]; then
526 check_ports
"${args[0]}"
527 elif [ "${#args[@]}" == "3" ]; then
528 check_ports
"${args[1]}"
532 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
533 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
541 # get hostname and settings from container configuration
542 for envar
in "${env[@]}"
544 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
546 # use as environment variable
552 hostname
=`hostname -s`
554 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
556 hostname
=$DISCOURSE_HOSTNAME
558 hostname
=$hostname-$config
561 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
562 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
563 # docker added more hostname rules
564 hostname
=${hostname/_/-}
567 $docker_path run
$user_args $links $attach_on_run $restart_policy "${env[@]}" -h "$hostname" \
568 -e DOCKER_HOST_IP
=$docker_ip --name $config -t $ports $volumes $docker_args $run_image $boot_command
575 valid_config_check
() {
578 for x
in DISCOURSE_SMTP_ADDRESS DISCOURSE_SMTP_USER_NAME DISCOURSE_SMTP_PASSWORD \
579 DISCOURSE_DEVELOPER_EMAILS DISCOURSE_HOSTNAME
581 mail_var
=`grep "^ $x:" $config_file`
583 local default
="example.com"
586 if [[ $mail_var = *"$default"* ]]
588 echo "Warning: $x left at incorrect default of example.com"
592 echo "Warning: $x not configured"
596 if [ -t 0 ] && [ "$valid_config" != "y" ]; then
598 read -p "Press Ctrl-C to exit and edit $config_file or ENTER to continue"
604 # Does your system meet the minimum requirements?
605 if [ "$opt" != "--skip-prereqs" ] ; then
611 # is our configuration file valid?
614 # make minor scaling adjustments for RAM and CPU
617 # I got no frigging clue what this does, ask Sam Saffron. It RUNS STUFF ON THE HOST I GUESS?
620 # Is the image available?
621 # If not, pull it here so the user is aware what's happening.
622 $docker_path history $image >/dev
/null
2>&1 ||
$docker_path pull
$image
626 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
627 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
629 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
630 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
632 if [[ ! X
"" = X
"$base_image" ]]; then
641 run_command
="cd /pups &&"
642 if [[ ! "false" = $update_pups ]]; then
643 run_command
="$run_command git pull &&"
645 run_command
="$run_command /pups/bin/pups --stdin"
649 (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 \
650 /bin
/bash
-c "$run_command") \
651 ||
($docker_path rm `cat $cidbootstrap` && rm $cidbootstrap)
653 [ ! -e $cidbootstrap ] && echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one" && exit 1
657 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
658 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
666 echo "Successfully bootstrapped, to startup use ./launcher start $config"
671 exec $docker_path exec -it $config /bin
/bash
--login
681 $docker_path logs
$config
697 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
698 echo "Ensuring discourse docker is up to date"
702 LOCAL
=$
(git rev-parse @
)
703 REMOTE
=$
(git rev-parse @
{u
})
704 BASE
=$
(git merge-base @ @
{u
})
706 if [ $LOCAL = $REMOTE ]; then
707 echo "Discourse Docker is up-to-date"
709 elif [ $LOCAL = $BASE ]; then
710 echo "Updating Discourse Docker"
711 git pull ||
(echo 'failed to update' && exit 1)
714 elif [ $REMOTE = $BASE ]; then
715 echo "Your version of Discourse Docker is ahead of origin"
718 echo "Discourse Docker has diverged source, this is only expected in Dev mode"
723 set_existing_container
725 if [ ! -z $existing ]
727 echo "Stopping old container"
730 $docker_path stop
-t 10 $config
736 if [ ! -z $existing ]
738 echo "Removing old container"
741 $docker_path rm $config
751 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)