├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md └── deploy.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'oj' 4 | gem 'rye' 5 | gem 'digitalocean' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | annoy (0.5.6) 5 | highline (>= 1.5.0) 6 | digitalocean (0.0.2) 7 | faraday 8 | faraday_middleware 9 | recursive-open-struct 10 | docile (1.1.1) 11 | drydock (0.6.9) 12 | faraday (0.8.8) 13 | multipart-post (~> 1.2.0) 14 | faraday_middleware (0.9.0) 15 | faraday (>= 0.7.4, < 0.9) 16 | highline (1.6.20) 17 | multipart-post (1.2.0) 18 | net-scp (1.1.2) 19 | net-ssh (>= 2.6.5) 20 | net-ssh (2.7.0) 21 | oj (2.2.3) 22 | recursive-open-struct (0.4.5) 23 | rye (0.9.11) 24 | annoy 25 | docile (>= 1.0.1) 26 | highline (>= 1.5.1) 27 | net-scp (>= 1.0.2) 28 | net-ssh (>= 2.0.13) 29 | sysinfo (>= 0.7.3) 30 | storable (0.8.9) 31 | sysinfo (0.8.0) 32 | drydock 33 | storable 34 | 35 | PLATFORMS 36 | ruby 37 | 38 | DEPENDENCIES 39 | digitalocean 40 | oj 41 | rye 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Robin Ward 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | discourse-droplet 2 | ================= 3 | 4 | A ruby script for installing Discourse on Digital Ocean using a simple wizard. 5 | Behind the scenes it makes heavy use of [discourse_docker](https://github.com/discourse/discourse_docker). 6 | 7 | Usage 8 | ===== 9 | 10 | ```bash 11 | $ bundle install 12 | $ ruby deploy.rb 13 | ``` 14 | 15 | Then you can go through the wizard like so: 16 | 17 | ```bash 18 | Your Digital Ocean Client id: 19 | YOUR_CLIENT_ID 20 | 21 | Your Digital Ocean API Key: 22 | YOUR_API_KEY 23 | 24 | Your developer email address: 25 | your.email@provider.com 26 | 27 | Host of Discourse forum (example: eviltrout.com) 28 | awesomeforum.com 29 | 30 | Confirm Your Settings 31 | ===================== 32 | Host: awesomeforum.com 33 | Email: asdf@.asdf.com 34 | SSH Key: Evil Trout 35 | 36 | Type 'Y' to continue 37 | Y 38 | 39 | 40 | ... a bunch of crazy output ... 41 | 42 | 43 | 44 | Discourse is ready to use: 45 | http://awesomeforum.com 46 | http://192.168.0.1 (the IP of your Droplet) 47 | 48 | ``` 49 | 50 | And you should be able to access your new forum in your browser. So easy! 51 | 52 | Next Steps, Upgrading, etc. 53 | =========================== 54 | 55 | For upgrading instructions, check out the [discourse_docker](https://github.com/discourse/discourse_docker). 56 | 57 | For more information about configuring Discourse, check out [discourse](http://github.com/discourse/discourse). 58 | 59 | License 60 | ======= 61 | 62 | MIT 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /deploy.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | Bundler.require(:default) 5 | 6 | Rye::Cmd.add_command :launcher, './launcher' 7 | Rye::Cmd.add_command :keygen, "ssh-keygen" 8 | Rye::Cmd.add_command :dd, "dd" 9 | Rye::Cmd.add_command :mkswap, "mkswap" 10 | Rye::Cmd.add_command :swapon, "swapon" 11 | Rye::Cmd.add_command :swap_fstab, 'echo "/swapfile none swap sw 0 0" >> /etc/fstab' 12 | Rye::Cmd.add_command :swapiness, 'echo 0 | tee /proc/sys/vm/swappiness' 13 | Rye::Cmd.add_command :apt_get, "apt-get" 14 | 15 | puts "Your Digital Ocean Client id:" 16 | Digitalocean.client_id = gets.chomp 17 | puts 18 | 19 | puts "Your Digital Ocean API Key:" 20 | Digitalocean.api_key = gets.chomp 21 | puts 22 | 23 | puts "Your developer email address:" 24 | email = gets.chomp 25 | puts 26 | 27 | puts "Host of Discourse forum: (example: eviltrout.com)" 28 | host = gets.chomp 29 | puts 30 | 31 | # TODO: ask for region 32 | # TODO: dynamically retrieve list of availables sizes depending on region 33 | 34 | sizes = { 35 | "1" => 63, 36 | "2" => 62, 37 | "3" => 64, 38 | "4" => 65, 39 | } 40 | 41 | size = "" 42 | until sizes.keys.include?(size) 43 | puts "Select size:" 44 | puts " 1. 1GB Memory, 1 Core, 30GB SSD Disk, 2TB Transfer, $10/month ($0.015/hour)" 45 | puts " 2. 2GB Memory, 2 Cores, 40GB SSD Disk, 3TB Transfer, $20/month ($0.030/hour)" 46 | puts " 3. 4GB Memory, 2 Cores, 60GB SSD Disk, 4TB Transfer, $40/month ($0.060/hour)" 47 | puts " 4. 8GB Memory, 4 Cores, 80GB SSD Disk, 5TB Transfer, $80/month ($0.119/hour)" 48 | size = gets.chomp 49 | puts 50 | end 51 | 52 | keys = Digitalocean::SshKey.all.ssh_keys 53 | 54 | if keys.nil? || keys.empty? 55 | puts "ERROR: You need to upload a ssh key to digital ocean and use working credentials" 56 | exit 57 | end 58 | 59 | ssh_key_names = keys.map { |k| k.name }.join(", ") 60 | ssh_key_ids = keys.map { |k| k.id } 61 | 62 | puts "SMTP Host: (empty for none, not recommended)" 63 | smtp_host = gets.chomp 64 | puts 65 | 66 | unless smtp_host.empty? 67 | puts "SMTP Port:" 68 | smtp_port = gets.chomp 69 | puts 70 | 71 | puts "SMTP Username:" 72 | smtp_username = gets.chomp 73 | puts 74 | 75 | puts "SMTP Password:" 76 | smtp_password = gets.chomp 77 | puts 78 | end 79 | 80 | puts "Confirm Your Settings" 81 | puts "=====================" 82 | puts "Email: #{email}" 83 | puts "Host: #{host}" 84 | puts "Size: The one with #{size}GB of memory" 85 | puts "SSH Key(s): #{ssh_key_names}" 86 | unless smtp_host.empty? 87 | puts "SMTP Host: #{smtp_host}" 88 | puts "SMTP Port: #{smtp_port}" 89 | puts "SMTP Username: #{smtp_username}" 90 | puts "SMTP Password: #{smtp_password}" 91 | end 92 | puts 93 | 94 | response = "" 95 | while response.downcase != 'y' 96 | puts "Type 'Y' to continue" 97 | response = gets.chomp 98 | end 99 | puts 100 | puts "Creating #{host}..." 101 | 102 | droplet = Digitalocean::Droplet.create(name: host, size_id: sizes[size].to_s, image_id: 3104894, region_id: 4, ssh_key_ids: ssh_key_ids).droplet 103 | droplet_id = droplet.id 104 | 105 | print "Waiting for #{host} (#{droplet_id}) to become active..." 106 | 107 | droplet = Digitalocean::Droplet.retrieve(droplet_id).droplet 108 | while droplet.status != 'active' 109 | sleep 5 110 | droplet = Digitalocean::Droplet.retrieve(droplet_id).droplet 111 | print '.' 112 | end 113 | puts 114 | 115 | puts "Removing any old SSH host entries (digital ocean reuses them)..." 116 | system "ssh-keygen -R #{droplet.ip_address}" if File.exists?(File.expand_path("~/.ssh/known_hosts")) 117 | 118 | puts "Initializing Droplet (#{droplet_id}) #{droplet.ip_address}..." 119 | attempts = 0 120 | begin 121 | rbox =Rye::Box.new(droplet.ip_address, user: 'root', timeout: 10) 122 | rbox.ls 123 | rescue Timeout::Error, Net::SSH::Disconnect, Errno::ECONNREFUSED 124 | attempts += 1 125 | if attempts < 20 126 | puts "Retrying SSH... Attempt: #{attempts}" 127 | sleep 10 128 | retry 129 | end 130 | puts "Couldn't connect via SSH" 131 | end 132 | 133 | puts "Creating Swap..." 134 | swap_count = size == "1" ? 2048 : 1024 # 2GB swap when 1GB RAM 135 | rbox.dd 'if=/dev/zero', 'of=/swapfile', 'bs=1024', "count=#{swap_count}k" 136 | rbox.mkswap '/swapfile' 137 | rbox.swapon "/swapfile" 138 | rbox.disable_safe_mode 139 | rbox.swap_fstab 140 | rbox.swapiness 141 | rbox.chown 'root:root', '/swapfile' 142 | rbox.chmod '0600', '/swapfile' 143 | rbox.enable_safe_mode 144 | 145 | puts "Checking out discourse_docker..." 146 | rbox.git 'clone', 'https://github.com/discourse/discourse_docker.git', '/var/docker' 147 | 148 | puts "Upgrading docker..." 149 | rbox.apt_get :y, :q, 'update' 150 | rbox.apt_get :y, :q, 'install', 'lxc-docker' 151 | 152 | rbox.cd '/var/docker' 153 | # Generate a SSH key to shell into docker with 154 | puts "Generating SSH key..." 155 | rbox.keygen '-t', 'rsa', '-f', '/root/.ssh/id_rsa', '-N', '' 156 | pub_key = rbox.cat("/root/.ssh/id_rsa.pub").to_s 157 | 158 | puts "Customizing config file..." 159 | config = YAML.load(rbox.cat("/var/docker/samples/standalone.yml").to_s) 160 | config['params']['ssh_key'] = pub_key 161 | config['env']['DISCOURSE_HOSTNAME'] = host 162 | config['env']['DISCOURSE_DEVELOPER_EMAILS'] = email 163 | 164 | unless smtp_host.empty? 165 | config['env']['DISCOURSE_SMTP_ADDRESS'] = smtp_host 166 | config['env']['DISCOURSE_SMTP_PORT'] = smtp_port 167 | config['env']['DISCOURSE_SMTP_USER_NAME'] = smtp_username 168 | config['env']['DISCOURSE_SMTP_PASSWORD'] = smtp_password 169 | end 170 | 171 | app_yml = StringIO.new(config.to_yaml) 172 | rbox.file_upload app_yml, "/var/docker/containers/app.yml" 173 | 174 | puts "Bootstrapping image..." 175 | rbox.cd '/var/docker' 176 | rbox.launcher 'bootstrap', 'app', '--skip-prereqs' 177 | 178 | puts "Starting Discourse..." 179 | rbox.launcher 'start', 'app', '--skip-prereqs' 180 | 181 | puts "Discourse is ready to use:" 182 | puts "http://#{host}" 183 | puts "http://#{droplet.ip_address}" 184 | puts 185 | puts "If you get a Gateway 502 error, try again in a few seconds; Rails is still likely starting up." 186 | --------------------------------------------------------------------------------