You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

459 lines
15 KiB

  1. # frozen_string_literal: true
  2. require 'tty-prompt'
  3. namespace :mastodon do
  4. desc 'Configure the instance for production use'
  5. task :setup do
  6. prompt = TTY::Prompt.new
  7. env = {}
  8. begin
  9. prompt.say('Your instance is identified by its domain name. Changing it afterward will break things.')
  10. env['LOCAL_DOMAIN'] = prompt.ask('Domain name:') do |q|
  11. q.required true
  12. q.modify :strip
  13. q.validate(/\A[a-z0-9\.\-]+\z/i)
  14. q.messages[:valid?] = 'Invalid domain. If you intend to use unicode characters, enter punycode here'
  15. end
  16. prompt.say "\n"
  17. prompt.say('Single user mode disables registrations and redirects the landing page to your public profile.')
  18. env['SINGLE_USER_MODE'] = prompt.yes?('Do you want to enable single user mode?', default: false)
  19. %w(SECRET_KEY_BASE OTP_SECRET).each do |key|
  20. env[key] = SecureRandom.hex(64)
  21. end
  22. vapid_key = Webpush.generate_key
  23. env['VAPID_PRIVATE_KEY'] = vapid_key.private_key
  24. env['VAPID_PUBLIC_KEY'] = vapid_key.public_key
  25. prompt.say "\n"
  26. using_docker = prompt.yes?('Are you using Docker to run Mastodon?')
  27. db_connection_works = false
  28. prompt.say "\n"
  29. loop do
  30. env['DB_HOST'] = prompt.ask('PostgreSQL host:') do |q|
  31. q.required true
  32. q.default using_docker ? 'db' : '/var/run/postgresql'
  33. q.modify :strip
  34. end
  35. env['DB_PORT'] = prompt.ask('PostgreSQL port:') do |q|
  36. q.required true
  37. q.default 5432
  38. q.convert :int
  39. end
  40. env['DB_NAME'] = prompt.ask('Name of PostgreSQL database:') do |q|
  41. q.required true
  42. q.default using_docker ? 'postgres' : 'mastodon_production'
  43. q.modify :strip
  44. end
  45. env['DB_USER'] = prompt.ask('Name of PostgreSQL user:') do |q|
  46. q.required true
  47. q.default using_docker ? 'postgres' : 'mastodon'
  48. q.modify :strip
  49. end
  50. env['DB_PASS'] = prompt.ask('Password of PostgreSQL user:') do |q|
  51. q.echo false
  52. end
  53. # The chosen database may not exist yet. Connect to default database
  54. # to avoid "database does not exist" error.
  55. db_options = {
  56. adapter: :postgresql,
  57. database: 'postgres',
  58. host: env['DB_HOST'],
  59. port: env['DB_PORT'],
  60. user: env['DB_USER'],
  61. password: env['DB_PASS'],
  62. }
  63. begin
  64. ActiveRecord::Base.establish_connection(db_options)
  65. ActiveRecord::Base.connection
  66. prompt.ok 'Database configuration works! 🎆'
  67. db_connection_works = true
  68. break
  69. rescue StandardError => e
  70. prompt.error 'Database connection could not be established with this configuration, try again.'
  71. prompt.error e.message
  72. break unless prompt.yes?('Try again?')
  73. end
  74. end
  75. prompt.say "\n"
  76. loop do
  77. env['REDIS_HOST'] = prompt.ask('Redis host:') do |q|
  78. q.required true
  79. q.default using_docker ? 'redis' : 'localhost'
  80. q.modify :strip
  81. end
  82. env['REDIS_PORT'] = prompt.ask('Redis port:') do |q|
  83. q.required true
  84. q.default 6379
  85. q.convert :int
  86. end
  87. env['REDIS_PASSWORD'] = prompt.ask('Redis password:') do |q|
  88. q.required false
  89. q.default nil
  90. q.modify :strip
  91. end
  92. redis_options = {
  93. host: env['REDIS_HOST'],
  94. port: env['REDIS_PORT'],
  95. password: env['REDIS_PASSWORD'],
  96. driver: :hiredis,
  97. }
  98. begin
  99. redis = Redis.new(redis_options)
  100. redis.ping
  101. prompt.ok 'Redis configuration works! 🎆'
  102. break
  103. rescue StandardError => e
  104. prompt.error 'Redis connection could not be established with this configuration, try again.'
  105. prompt.error e.message
  106. break unless prompt.yes?('Try again?')
  107. end
  108. end
  109. prompt.say "\n"
  110. if prompt.yes?('Do you want to store uploaded files on the cloud?', default: false)
  111. case prompt.select('Provider', ['Amazon S3', 'Wasabi', 'Minio', 'Google Cloud Storage'])
  112. when 'Amazon S3'
  113. env['S3_ENABLED'] = 'true'
  114. env['S3_PROTOCOL'] = 'https'
  115. env['S3_BUCKET'] = prompt.ask('S3 bucket name:') do |q|
  116. q.required true
  117. q.default "files.#{env['LOCAL_DOMAIN']}"
  118. q.modify :strip
  119. end
  120. env['S3_REGION'] = prompt.ask('S3 region:') do |q|
  121. q.required true
  122. q.default 'us-east-1'
  123. q.modify :strip
  124. end
  125. env['S3_HOSTNAME'] = prompt.ask('S3 hostname:') do |q|
  126. q.required true
  127. q.default 's3-us-east-1.amazonaws.com'
  128. q.modify :strip
  129. end
  130. env['AWS_ACCESS_KEY_ID'] = prompt.ask('S3 access key:') do |q|
  131. q.required true
  132. q.modify :strip
  133. end
  134. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('S3 secret key:') do |q|
  135. q.required true
  136. q.modify :strip
  137. end
  138. when 'Wasabi'
  139. env['S3_ENABLED'] = 'true'
  140. env['S3_PROTOCOL'] = 'https'
  141. env['S3_REGION'] = 'us-east-1'
  142. env['S3_HOSTNAME'] = 's3.wasabisys.com'
  143. env['S3_ENDPOINT'] = 'https://s3.wasabisys.com/'
  144. env['S3_BUCKET'] = prompt.ask('Wasabi bucket name:') do |q|
  145. q.required true
  146. q.default "files.#{env['LOCAL_DOMAIN']}"
  147. q.modify :strip
  148. end
  149. env['AWS_ACCESS_KEY_ID'] = prompt.ask('Wasabi access key:') do |q|
  150. q.required true
  151. q.modify :strip
  152. end
  153. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Wasabi secret key:') do |q|
  154. q.required true
  155. q.modify :strip
  156. end
  157. when 'Minio'
  158. env['S3_ENABLED'] = 'true'
  159. env['S3_PROTOCOL'] = 'https'
  160. env['S3_REGION'] = 'us-east-1'
  161. env['S3_ENDPOINT'] = prompt.ask('Minio endpoint URL:') do |q|
  162. q.required true
  163. q.modify :strip
  164. end
  165. env['S3_PROTOCOL'] = env['S3_ENDPOINT'].start_with?('https') ? 'https' : 'http'
  166. env['S3_HOSTNAME'] = env['S3_ENDPOINT'].gsub(/\Ahttps?:\/\//, '')
  167. env['S3_BUCKET'] = prompt.ask('Minio bucket name:') do |q|
  168. q.required true
  169. q.default "files.#{env['LOCAL_DOMAIN']}"
  170. q.modify :strip
  171. end
  172. env['AWS_ACCESS_KEY_ID'] = prompt.ask('Minio access key:') do |q|
  173. q.required true
  174. q.modify :strip
  175. end
  176. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Minio secret key:') do |q|
  177. q.required true
  178. q.modify :strip
  179. end
  180. when 'Google Cloud Storage'
  181. env['S3_ENABLED'] = 'true'
  182. env['S3_PROTOCOL'] = 'https'
  183. env['S3_HOSTNAME'] = 'storage.googleapis.com'
  184. env['S3_ENDPOINT'] = 'https://storage.googleapis.com'
  185. env['S3_MULTIPART_THRESHOLD'] = 50.megabytes
  186. env['S3_BUCKET'] = prompt.ask('GCS bucket name:') do |q|
  187. q.required true
  188. q.default "files.#{env['LOCAL_DOMAIN']}"
  189. q.modify :strip
  190. end
  191. env['S3_REGION'] = prompt.ask('GCS region:') do |q|
  192. q.required true
  193. q.default 'us-west1'
  194. q.modify :strip
  195. end
  196. env['AWS_ACCESS_KEY_ID'] = prompt.ask('GCS access key:') do |q|
  197. q.required true
  198. q.modify :strip
  199. end
  200. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('GCS secret key:') do |q|
  201. q.required true
  202. q.modify :strip
  203. end
  204. end
  205. if prompt.yes?('Do you want to access the uploaded files from your own domain?')
  206. env['S3_ALIAS_HOST'] = prompt.ask('Domain for uploaded files:') do |q|
  207. q.required true
  208. q.default "files.#{env['LOCAL_DOMAIN']}"
  209. q.modify :strip
  210. end
  211. end
  212. end
  213. prompt.say "\n"
  214. loop do
  215. if prompt.yes?('Do you want to send e-mails from localhost?', default: false)
  216. env['SMTP_SERVER'] = 'localhost'
  217. env['SMTP_PORT'] = 25
  218. env['SMTP_AUTH_METHOD'] = 'none'
  219. env['SMTP_OPENSSL_VERIFY_MODE'] = 'none'
  220. else
  221. env['SMTP_SERVER'] = prompt.ask('SMTP server:') do |q|
  222. q.required true
  223. q.default 'smtp.mailgun.org'
  224. q.modify :strip
  225. end
  226. env['SMTP_PORT'] = prompt.ask('SMTP port:') do |q|
  227. q.required true
  228. q.default 587
  229. q.convert :int
  230. end
  231. env['SMTP_LOGIN'] = prompt.ask('SMTP username:') do |q|
  232. q.modify :strip
  233. end
  234. env['SMTP_PASSWORD'] = prompt.ask('SMTP password:') do |q|
  235. q.echo false
  236. end
  237. env['SMTP_AUTH_METHOD'] = prompt.ask('SMTP authentication:') do |q|
  238. q.required
  239. q.default 'plain'
  240. q.modify :strip
  241. end
  242. env['SMTP_OPENSSL_VERIFY_MODE'] = prompt.select('SMTP OpenSSL verify mode:', %w(none peer client_once fail_if_no_peer_cert))
  243. end
  244. env['SMTP_FROM_ADDRESS'] = prompt.ask('E-mail address to send e-mails "from":') do |q|
  245. q.required true
  246. q.default "Mastodon <notifications@#{env['LOCAL_DOMAIN']}>"
  247. q.modify :strip
  248. end
  249. break unless prompt.yes?('Send a test e-mail with this configuration right now?')
  250. send_to = prompt.ask('Send test e-mail to:', required: true)
  251. begin
  252. ActionMailer::Base.smtp_settings = {
  253. port: env['SMTP_PORT'],
  254. address: env['SMTP_SERVER'],
  255. user_name: env['SMTP_LOGIN'].presence,
  256. password: env['SMTP_PASSWORD'].presence,
  257. domain: env['LOCAL_DOMAIN'],
  258. authentication: env['SMTP_AUTH_METHOD'] == 'none' ? nil : env['SMTP_AUTH_METHOD'] || :plain,
  259. openssl_verify_mode: env['SMTP_OPENSSL_VERIFY_MODE'],
  260. enable_starttls_auto: true,
  261. }
  262. ActionMailer::Base.default_options = {
  263. from: env['SMTP_FROM_ADDRESS'],
  264. }
  265. mail = ActionMailer::Base.new.mail to: send_to, subject: 'Test', body: 'Mastodon SMTP configuration works!'
  266. mail.deliver
  267. break
  268. rescue StandardError => e
  269. prompt.error 'E-mail could not be sent with this configuration, try again.'
  270. prompt.error e.message
  271. break unless prompt.yes?('Try again?')
  272. end
  273. end
  274. prompt.say "\n"
  275. prompt.say 'This configuration will be written to .env.production'
  276. if prompt.yes?('Save configuration?')
  277. incompatible_syntax = false
  278. env_contents = env.each_pair.map do |key, value|
  279. if value.is_a?(String) && value =~ /[\s\#\\"]/
  280. incompatible_syntax = true
  281. if value =~ /[']/
  282. value = value.to_s.gsub(/[\\"\$]/) { |x| "\\#{x}" }
  283. "#{key}=\"#{value}\""
  284. else
  285. "#{key}='#{value}'"
  286. end
  287. else
  288. "#{key}=#{value}"
  289. end
  290. end.join("\n")
  291. generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n".dup
  292. if incompatible_syntax
  293. generated_header << "# Some variables in this file will be interpreted differently whether you are\n"
  294. generated_header << "# using docker-compose or not.\n\n"
  295. end
  296. File.write(Rails.root.join('.env.production'), "#{generated_header}#{env_contents}\n")
  297. if using_docker
  298. prompt.ok 'Below is your configuration, save it to an .env.production file outside Docker:'
  299. prompt.say "\n"
  300. prompt.say "#{generated_header}#{env.each_pair.map { |key, value| "#{key}=#{value}" }.join("\n")}"
  301. prompt.say "\n"
  302. prompt.ok 'It is also saved within this container so you can proceed with this wizard.'
  303. end
  304. prompt.say "\n"
  305. prompt.say 'Now that configuration is saved, the database schema must be loaded.'
  306. prompt.warn 'If the database already exists, this will erase its contents.'
  307. if prompt.yes?('Prepare the database now?')
  308. prompt.say 'Running `RAILS_ENV=production rails db:setup` ...'
  309. prompt.say "\n\n"
  310. if !system(env.transform_values(&:to_s).merge({ 'RAILS_ENV' => 'production', 'SAFETY_ASSURED' => '1' }), 'rails db:setup')
  311. prompt.error 'That failed! Perhaps your configuration is not right'
  312. else
  313. prompt.ok 'Done!'
  314. end
  315. end
  316. unless using_docker
  317. prompt.say "\n"
  318. prompt.say 'The final step is compiling CSS/JS assets.'
  319. prompt.say 'This may take a while and consume a lot of RAM.'
  320. if prompt.yes?('Compile the assets now?')
  321. prompt.say 'Running `RAILS_ENV=production rails assets:precompile` ...'
  322. prompt.say "\n\n"
  323. if !system(env.transform_values(&:to_s).merge({ 'RAILS_ENV' => 'production' }), 'rails assets:precompile')
  324. prompt.error 'That failed! Maybe you need swap space?'
  325. else
  326. prompt.say 'Done!'
  327. end
  328. end
  329. end
  330. prompt.say "\n"
  331. prompt.ok 'All done! You can now power on the Mastodon server 🐘'
  332. prompt.say "\n"
  333. if db_connection_works && prompt.yes?('Do you want to create an admin user straight away?')
  334. env.each_pair do |key, value|
  335. ENV[key] = value.to_s
  336. end
  337. require_relative '../../config/environment'
  338. disable_log_stdout!
  339. username = prompt.ask('Username:') do |q|
  340. q.required true
  341. q.default 'admin'
  342. q.validate(/\A[a-z0-9_]+\z/i)
  343. q.modify :strip
  344. end
  345. email = prompt.ask('E-mail:') do |q|
  346. q.required true
  347. q.modify :strip
  348. end
  349. password = SecureRandom.hex(16)
  350. user = User.new(admin: true, email: email, password: password, confirmed_at: Time.now.utc, account_attributes: { username: username }, bypass_invite_request_check: true)
  351. user.save(validate: false)
  352. prompt.ok "You can login with the password: #{password}"
  353. prompt.warn 'You can change your password once you login.'
  354. end
  355. else
  356. prompt.warn 'Nothing saved. Bye!'
  357. end
  358. rescue TTY::Reader::InputInterrupt
  359. prompt.ok 'Aborting. Bye!'
  360. end
  361. end
  362. namespace :webpush do
  363. desc 'Generate VAPID key'
  364. task generate_vapid_key: :environment do
  365. vapid_key = Webpush.generate_key
  366. puts "VAPID_PRIVATE_KEY=#{vapid_key.private_key}"
  367. puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}"
  368. end
  369. end
  370. end
  371. def disable_log_stdout!
  372. dev_null = Logger.new('/dev/null')
  373. Rails.logger = dev_null
  374. ActiveRecord::Base.logger = dev_null
  375. HttpLog.configuration.logger = dev_null
  376. Paperclip.options[:log] = false
  377. end