docker_rails_proxy 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/docker_rails_proxy.rb +33 -2
- data/lib/docker_rails_proxy/commands/build.rb +16 -2
- data/lib/docker_rails_proxy/commands/compose/up.rb +1 -1
- data/lib/docker_rails_proxy/commands/stack.rb +98 -0
- data/lib/docker_rails_proxy/commands/stack/create.rb +181 -0
- data/lib/docker_rails_proxy/commands/stack/destroy.rb +48 -0
- data/lib/docker_rails_proxy/concerns/rsync.rb +2 -0
- data/lib/docker_rails_proxy/extends/fixnum_support.rb +9 -0
- data/lib/docker_rails_proxy/extends/nil_class_support.rb +13 -0
- data/lib/docker_rails_proxy/extends/string_support.rb +18 -5
- data/lib/docker_rails_proxy/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 78dfed44ff1431c3e5216b9d354a1dd208b5f479
|
4
|
+
data.tar.gz: 8e5b2f9ec42d2a826e3814cf6c122a33905b5273
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc5c27596574efc2690de74f50d9c2a4bfe28f9c0badbc2f64bc112c4ffb1026d68ea625add78bf4109e30460cee2cfda3382e2d8ab5fd6878fd369b0f8acebe
|
7
|
+
data.tar.gz: 8432d2afe6b3a3a91c0e1127963270617dde46b7a4983648ea9559567b4a129c0ddc5fc1ccfdcb857697fca25797c77516871461295bba13152acf2410ebd08e
|
data/lib/docker_rails_proxy.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
|
2
|
-
require
|
1
|
+
Dir[File.expand_path('../docker_rails_proxy/extends/*.rb', __FILE__)].map do |f|
|
2
|
+
require f
|
3
|
+
end
|
3
4
|
|
4
5
|
module DockerRailsProxy
|
5
6
|
COMMANDS = Dir[File.expand_path('../docker_rails_proxy/commands/*.rb', __FILE__)].map do |f|
|
@@ -33,6 +34,16 @@ module DockerRailsProxy
|
|
33
34
|
File.join(APP_PATH, block_given? ? yield : path)
|
34
35
|
end
|
35
36
|
|
37
|
+
def command
|
38
|
+
name.sub('DockerRailsProxy::', ''.freeze).parameterize.sub('--', ' '.freeze)
|
39
|
+
end
|
40
|
+
|
41
|
+
def execute(options)
|
42
|
+
system <<-EOS
|
43
|
+
#{build_path("bin/#{APP_NAME}")} #{command} #{options}
|
44
|
+
EOS
|
45
|
+
end
|
46
|
+
|
36
47
|
def call(options)
|
37
48
|
klass = _run_build_callbacks params: options
|
38
49
|
|
@@ -62,6 +73,26 @@ module DockerRailsProxy
|
|
62
73
|
def build_path(*args, &block)
|
63
74
|
self.class.build_path(*args, &block)
|
64
75
|
end
|
76
|
+
|
77
|
+
def print_options(values, message)
|
78
|
+
puts message
|
79
|
+
values.each_with_index { |v, i| puts "#{i}) #{v}" }
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_option(values, default = nil)
|
83
|
+
flush_stdin
|
84
|
+
print ": "
|
85
|
+
option = $stdin.gets.chomp
|
86
|
+
|
87
|
+
return default if option.blank?
|
88
|
+
option =~ /^\d+$/ ? values[option.to_i] : nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def flush_stdin
|
92
|
+
loop do
|
93
|
+
Timeout::timeout(0.1) { $stdin.gets.chomp } rescue break
|
94
|
+
end
|
95
|
+
end
|
65
96
|
end
|
66
97
|
|
67
98
|
class AwsCli < Base
|
@@ -5,7 +5,7 @@ module DockerRailsProxy
|
|
5
5
|
attr_accessor :options
|
6
6
|
|
7
7
|
after_initialize { self.options = {} }
|
8
|
-
after_initialize :parse_options
|
8
|
+
after_initialize :parse_options!, :set_defaults
|
9
9
|
|
10
10
|
validates do
|
11
11
|
if options[:dockerfile].nil?
|
@@ -17,6 +17,12 @@ module DockerRailsProxy
|
|
17
17
|
|
18
18
|
validates { '--tag is required' if options[:tag].nil? }
|
19
19
|
|
20
|
+
validates do
|
21
|
+
unless File.directory?(options[:context])
|
22
|
+
"#{options[:context]} folder does not exist."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
20
26
|
validates do
|
21
27
|
options[:build_args] ||= {}
|
22
28
|
|
@@ -37,7 +43,7 @@ module DockerRailsProxy
|
|
37
43
|
-f '#{options[:dockerfile]}' \
|
38
44
|
-t '#{options[:tag]}' \
|
39
45
|
#{build_args} \
|
40
|
-
'#{
|
46
|
+
'#{options[:context]}'
|
41
47
|
EOS
|
42
48
|
end
|
43
49
|
|
@@ -49,6 +55,10 @@ module DockerRailsProxy
|
|
49
55
|
end.join(' ')
|
50
56
|
end
|
51
57
|
|
58
|
+
def set_defaults
|
59
|
+
options[:context] ||= File.dirname(options[:dockerfile])
|
60
|
+
end
|
61
|
+
|
52
62
|
def parse_options!
|
53
63
|
opt_parser.parse!(arguments)
|
54
64
|
end
|
@@ -61,6 +71,10 @@ module DockerRailsProxy
|
|
61
71
|
options[:dockerfile] = dockerfile
|
62
72
|
end
|
63
73
|
|
74
|
+
opts.on('--context [CONTEXT]', 'Docker build context') do |context|
|
75
|
+
options[:context] = context
|
76
|
+
end
|
77
|
+
|
64
78
|
opts.on('--tag TAG', 'Docker Image Tag') { |tag| options[:tag] = tag }
|
65
79
|
|
66
80
|
opts.on('--build-args A=val,B=val...', Array, 'Docker build-args') do |o|
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module DockerRailsProxy
|
4
|
+
class Stack < AwsCli
|
5
|
+
autoload :Create, 'docker_rails_proxy/commands/stack/create'
|
6
|
+
autoload :Destroy, 'docker_rails_proxy/commands/stack/destroy'
|
7
|
+
|
8
|
+
RUNNING_STATUSES = %w[CREATE_COMPLETE UPDATE_COMPLETE].freeze
|
9
|
+
|
10
|
+
attr_accessor :options
|
11
|
+
|
12
|
+
before_initialize do
|
13
|
+
'jq is required, `brew install jq`' unless system 'type jq &> /dev/null'
|
14
|
+
end
|
15
|
+
|
16
|
+
after_initialize { self.options = {} }
|
17
|
+
after_initialize { opt_parser.parse!(arguments) }
|
18
|
+
after_initialize { options[:profile] ||= APP_NAME }
|
19
|
+
|
20
|
+
validates { '--profile is required.' if options[:profile].blank? }
|
21
|
+
|
22
|
+
builds -> (params:) do
|
23
|
+
case params[:arguments].shift
|
24
|
+
when 'create' then Create
|
25
|
+
when 'destroy' then Destroy
|
26
|
+
when 'deploy'
|
27
|
+
klass_name = %W[
|
28
|
+
DockerRailsProxy
|
29
|
+
Stack
|
30
|
+
#{params[:arguments].first.classify}Deploy
|
31
|
+
].join('::')
|
32
|
+
|
33
|
+
begin
|
34
|
+
klass_name.constantize
|
35
|
+
rescue
|
36
|
+
$stderr.puts "#{klass_name} class does not exit"
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
else
|
40
|
+
puts "Usage: bin/#{APP_NAME} stack <create|destroy|deploy> [options]"
|
41
|
+
exit
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def stack_exist?(stack_name)
|
48
|
+
!stack_status(stack_name).blank?
|
49
|
+
end
|
50
|
+
|
51
|
+
def wait_for_stack(stack_name)
|
52
|
+
loop do
|
53
|
+
case status = stack_status(stack_name)
|
54
|
+
when 'CREATE_IN_PROGRESS', 'UPDATE_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS'
|
55
|
+
puts "#{stack_name} still processing: #{status}"
|
56
|
+
|
57
|
+
when *RUNNING_STATUSES
|
58
|
+
puts "#{stack_name} stack stabilized: #{status}"
|
59
|
+
break
|
60
|
+
|
61
|
+
else
|
62
|
+
$stderr.puts %{
|
63
|
+
There is a problem with the #{stack_name} stack: #{status}
|
64
|
+
}
|
65
|
+
exit 1
|
66
|
+
end
|
67
|
+
|
68
|
+
sleep 10
|
69
|
+
end if stack_exist?(stack_name)
|
70
|
+
end
|
71
|
+
|
72
|
+
def stack_status(stack_name)
|
73
|
+
%x(aws cloudformation describe-stacks \
|
74
|
+
--profile '#{options[:profile]}' \
|
75
|
+
| jq '.Stacks[] | select(.StackName == "#{stack_name}") | .StackStatus' \
|
76
|
+
| xargs
|
77
|
+
).strip
|
78
|
+
end
|
79
|
+
|
80
|
+
def opt_parser
|
81
|
+
@opt_parser ||= OptionParser.new do |opts|
|
82
|
+
opts.banner = "Usage: bin/#{APP_NAME} stack #{self.class.name.demodulize.parameterize} [options]"
|
83
|
+
|
84
|
+
opts.on(
|
85
|
+
'--profile [PROFILE]',
|
86
|
+
"Aws profile (Default: #{APP_NAME})"
|
87
|
+
) { |profile| options[:profile] = profile }
|
88
|
+
|
89
|
+
yield opts if block_given?
|
90
|
+
|
91
|
+
opts.on('-h', '--help', 'Display this screen') do
|
92
|
+
puts opts
|
93
|
+
exit
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'timeout'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module DockerRailsProxy
|
6
|
+
class Stack < AwsCli
|
7
|
+
class Create < self
|
8
|
+
YML_EXTENSIONS = %w(.yml .yaml).freeze
|
9
|
+
|
10
|
+
attr_accessor :data, :parameters, :outputs
|
11
|
+
|
12
|
+
after_initialize { self.parameters, self.outputs = {}, {} }
|
13
|
+
after_initialize :set_defaults
|
14
|
+
|
15
|
+
validates { '--stack-name is required.' if options[:stack_name].blank? }
|
16
|
+
validates { '--ymlfile is required.' if options[:ymlfile].blank? }
|
17
|
+
|
18
|
+
validates do
|
19
|
+
unless File.exist? options[:ymlfile]
|
20
|
+
"#{options[:ymlfile]} file does not exit"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
validates do
|
25
|
+
if YML_EXTENSIONS.include? File.extname(options[:ymlfile])
|
26
|
+
self.data = YAML::load_file(options[:ymlfile])
|
27
|
+
options[:jsonfile] = options[:ymlfile].sub(/\..+/, '.json')
|
28
|
+
File.write(options[:jsonfile], data.to_json)
|
29
|
+
else
|
30
|
+
"#{options[:ymlfile]} is not a yml file"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
validates do
|
35
|
+
if options[:wait_for_stack]
|
36
|
+
wait_for_stack(options[:stack_name])
|
37
|
+
'' # Just exit, wait_for_stack will print messages
|
38
|
+
else
|
39
|
+
"Stack: #{options[:stack_name]} already exists"
|
40
|
+
end if stack_exist?(options[:stack_name])
|
41
|
+
end
|
42
|
+
|
43
|
+
validates do
|
44
|
+
unless system <<-EOS
|
45
|
+
aws cloudformation validate-template \
|
46
|
+
--template-body 'file://#{options[:jsonfile]}' \
|
47
|
+
--profile '#{options[:profile]}' \
|
48
|
+
> /dev/null
|
49
|
+
EOS
|
50
|
+
|
51
|
+
%{
|
52
|
+
Invalid template. See above errors
|
53
|
+
}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
before_process { set_outputs unless options[:import_outputs_from].empty? }
|
58
|
+
before_process { set_parameters }
|
59
|
+
|
60
|
+
after_process { File.delete(options[:jsonfile]) if File.exist?(options[:jsonfile]) }
|
61
|
+
after_process { wait_for_stack(options[:stack_name]) if options[:wait_for_stack] }
|
62
|
+
|
63
|
+
def process
|
64
|
+
system <<-EOS
|
65
|
+
aws cloudformation create-stack \
|
66
|
+
--stack-name '#{options[:stack_name]}' \
|
67
|
+
--parameters #{parameters.join(' ')} \
|
68
|
+
--template-body 'file://#{options[:jsonfile]}' \
|
69
|
+
--capabilities 'CAPABILITY_IAM' \
|
70
|
+
--profile '#{options[:profile]}'
|
71
|
+
EOS
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def set_outputs
|
77
|
+
jq_command = <<-EOS
|
78
|
+
.Stacks[]
|
79
|
+
| select(
|
80
|
+
.StackName as $name
|
81
|
+
| "#{options[:import_outputs_from].join(' ')}"
|
82
|
+
| split(" ")
|
83
|
+
| map(. == $name)
|
84
|
+
| index(true) >= 0
|
85
|
+
)
|
86
|
+
| .Outputs[]
|
87
|
+
| [ .OutputKey, .OutputValue ]
|
88
|
+
| join("=")
|
89
|
+
EOS
|
90
|
+
|
91
|
+
outputs_data = %x(
|
92
|
+
aws cloudformation describe-stacks --profile '#{options[:profile]}' \
|
93
|
+
| jq '#{jq_command}' | xargs
|
94
|
+
).strip.split(' ')
|
95
|
+
|
96
|
+
outputs_data.each do |string|
|
97
|
+
key, value = string.split('=')
|
98
|
+
self.outputs[key] = value
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def set_parameters
|
103
|
+
puts '-' * 100
|
104
|
+
puts "- #{options[:stack_name]} stack parameters"
|
105
|
+
|
106
|
+
(data['Parameters'] || {}).each do |key, attrs|
|
107
|
+
parameters[key] = options[:parameters][key]
|
108
|
+
parameters[key] ||= outputs[key]
|
109
|
+
|
110
|
+
next if parameters[key].present?
|
111
|
+
|
112
|
+
puts '-' * 100
|
113
|
+
|
114
|
+
while parameters[key].to_s.blank? do
|
115
|
+
value = nil
|
116
|
+
|
117
|
+
case attrs['Type']
|
118
|
+
when 'AWS::EC2::KeyPair::KeyName'
|
119
|
+
key_pairs ||= %x(
|
120
|
+
aws ec2 describe-key-pairs --profile '#{options[:profile]}' \
|
121
|
+
| jq '.KeyPairs[] | .KeyName' | xargs
|
122
|
+
).strip.split(' ')
|
123
|
+
|
124
|
+
print_options(key_pairs, "Choose an option for #{key} and press [ENTER]")
|
125
|
+
parameters[key] = get_option(key_pairs, value)
|
126
|
+
|
127
|
+
else
|
128
|
+
value ||= attrs['Default']
|
129
|
+
|
130
|
+
allowed_values = Array(attrs['AllowedValues'])
|
131
|
+
|
132
|
+
if allowed_values.empty?
|
133
|
+
print "Enter #{key} value and press [ENTER] (Default: #{value}): "
|
134
|
+
flush_stdin
|
135
|
+
|
136
|
+
parameters[key] = $stdin.gets.chomp || value
|
137
|
+
parameters[key] = value if parameters[key].blank?
|
138
|
+
else
|
139
|
+
print_options(allowed_values, "Choose an option for #{key} and press [ENTER] (Default: #{value})")
|
140
|
+
parameters[key] = get_option(allowed_values, value)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
self.parameters = parameters.map do |key, value|
|
147
|
+
"ParameterKey=\"#{key}\",ParameterValue=\"#{value}\",UsePreviousValue=false"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def set_defaults
|
152
|
+
options[:parameters] ||= {}
|
153
|
+
options[:import_outputs_from] ||= []
|
154
|
+
end
|
155
|
+
|
156
|
+
def opt_parser
|
157
|
+
super do |opts|
|
158
|
+
opts.on('--stack-name STACK_NAME', 'Stack Name') do |stack_name|
|
159
|
+
options[:stack_name] = stack_name
|
160
|
+
end
|
161
|
+
|
162
|
+
opts.on('--ymlfile YMLFILE', 'Stack YML file') do |ymlfile|
|
163
|
+
options[:ymlfile] = build_path(ymlfile)
|
164
|
+
end
|
165
|
+
|
166
|
+
opts.on('--parameters A=val,B=val...', Array, 'CF parameters') do |o|
|
167
|
+
options[:parameters] = Hash[o.map { |s| s.split('=', 2) }]
|
168
|
+
end
|
169
|
+
|
170
|
+
opts.on('--import-outputs-from a,b...', Array, 'CF stack names') do |list|
|
171
|
+
options[:import_outputs_from] = list
|
172
|
+
end
|
173
|
+
|
174
|
+
opts.on('--wait-for-stack', 'Wait for the stack to be created') do |v|
|
175
|
+
options[:wait_for_stack] = v
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module DockerRailsProxy
|
2
|
+
class Stack < AwsCli
|
3
|
+
class Destroy < self
|
4
|
+
attr_accessor :stacks
|
5
|
+
|
6
|
+
before_process do
|
7
|
+
jq_command = <<-EOS
|
8
|
+
.Stacks
|
9
|
+
| map(
|
10
|
+
select(
|
11
|
+
.StackStatus | inside("CREATE_COMPLETE UPDATE_COMPLETE")
|
12
|
+
)
|
13
|
+
| .StackName
|
14
|
+
)
|
15
|
+
| sort[]
|
16
|
+
EOS
|
17
|
+
|
18
|
+
self.stacks = %x(
|
19
|
+
aws cloudformation describe-stacks --profile '#{options[:profile]}' \
|
20
|
+
| jq '#{jq_command}' | xargs
|
21
|
+
).strip.split(' ')
|
22
|
+
|
23
|
+
if stacks.empty?
|
24
|
+
$stderr.puts 'There are no stacks running'
|
25
|
+
exit 1
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def process
|
30
|
+
begin
|
31
|
+
print_options(stacks, 'Choose the stack number and press [ENTER]')
|
32
|
+
stack_name = get_option(stacks)
|
33
|
+
end while stack_name.blank?
|
34
|
+
|
35
|
+
puts "You're about to destroy this stack: #{stack_name}, are you sure? [yes]:"
|
36
|
+
exit unless $stdin.gets.chomp == 'yes'
|
37
|
+
|
38
|
+
puts "Destroying #{stack_name} stack"
|
39
|
+
|
40
|
+
system <<-EOS
|
41
|
+
aws cloudformation delete-stack \
|
42
|
+
--stack-name '#{stack_name}' \
|
43
|
+
--profile '#{options[:profile]}'
|
44
|
+
EOS
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -1,12 +1,13 @@
|
|
1
1
|
class String
|
2
|
-
def
|
3
|
-
|
2
|
+
def blank?
|
3
|
+
nil? or empty?
|
4
4
|
end
|
5
5
|
|
6
6
|
def classify
|
7
|
-
gsub(
|
8
|
-
.gsub(
|
9
|
-
.
|
7
|
+
gsub('-'.freeze, '_'.freeze)
|
8
|
+
.gsub(/\W/, ''.freeze)
|
9
|
+
.split('_'.freeze)
|
10
|
+
.map{|s| s.sub(/^[a-z\d]*/, &:capitalize) }.join
|
10
11
|
end
|
11
12
|
|
12
13
|
def constantize
|
@@ -15,6 +16,18 @@ class String
|
|
15
16
|
end
|
16
17
|
end
|
17
18
|
|
19
|
+
def demodulize
|
20
|
+
split('::').last
|
21
|
+
end
|
22
|
+
|
23
|
+
def parameterize(separator = '-'.freeze)
|
24
|
+
downcase.gsub(/\W/, separator).gsub('_'.freeze, separator)
|
25
|
+
end
|
26
|
+
|
27
|
+
def present?
|
28
|
+
!empty?
|
29
|
+
end
|
30
|
+
|
18
31
|
def underscore
|
19
32
|
downcase.gsub('::'.freeze, '/'.freeze).gsub('-'.freeze, '_'.freeze)
|
20
33
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: docker_rails_proxy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jairo
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2016-
|
12
|
+
date: 2016-11-04 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Configures docker-compose and provides rails command helpers
|
15
15
|
email:
|
@@ -36,10 +36,15 @@ files:
|
|
36
36
|
- lib/docker_rails_proxy/commands/rspec.rb
|
37
37
|
- lib/docker_rails_proxy/commands/spring.rb
|
38
38
|
- lib/docker_rails_proxy/commands/ssh.rb
|
39
|
+
- lib/docker_rails_proxy/commands/stack.rb
|
40
|
+
- lib/docker_rails_proxy/commands/stack/create.rb
|
41
|
+
- lib/docker_rails_proxy/commands/stack/destroy.rb
|
39
42
|
- lib/docker_rails_proxy/concerns/callbacks.rb
|
40
43
|
- lib/docker_rails_proxy/concerns/inheritable_attributes.rb
|
41
44
|
- lib/docker_rails_proxy/concerns/rsync.rb
|
42
45
|
- lib/docker_rails_proxy/extends/colorization.rb
|
46
|
+
- lib/docker_rails_proxy/extends/fixnum_support.rb
|
47
|
+
- lib/docker_rails_proxy/extends/nil_class_support.rb
|
43
48
|
- lib/docker_rails_proxy/extends/string_support.rb
|
44
49
|
- lib/docker_rails_proxy/version.rb
|
45
50
|
homepage: https://github.com/jairovm/docker_rails_proxy
|