Skip to content

Instantly share code, notes, and snippets.

@steigr
Last active March 14, 2023 19:37
Show Gist options
  • Save steigr/8f02f4a135e8250ef4c8a70c18b4d561 to your computer and use it in GitHub Desktop.
Save steigr/8f02f4a135e8250ef4c8a70c18b4d561 to your computer and use it in GitHub Desktop.
Shellscript to build Rails Application Container real fast build time, once built
#!/usr/bin/env bash
_docker_entrypoint() {
cat <<'_docker_entrypoint'
#!/usr/bin/env bash
# error handling
set -eo pipefail
trap exit exit
pidof bundle >/dev/null 2>&1 && APP_IS_RUNNING=1
# Debugging
[[ -z "$APP_IS_RUNNING" ]] || ( [[ -z "$TRACE" ]] || set -x )
[[ -z "$APP_IS_RUNNING" ]] || ( [[ -z "$TRACE" ]] || printenv )
pidof tini >/dev/null 2>&1 || exec tini -- "$0" "$@"
# global settings
vars() {
export RAILS_USER=${RAILS_USER:-application}
export LOADER_TITLE=${LOADER_TITLE:-Application}
}
# create runtime directories
prepare_fs() {
mkdir -p /app/tmp/sockets \
/app/tmp/pids
}
# set application secret
prepare_secret() {
export SECRET_KEY_BASE=${SECRET_KEY_BASE:-$(bundle exec rake secret)}
}
# check if postgresql instance has been linked, return if not, else export DATABASE_URL
use_postgresql() {
printenv | grep -q '_ENV_PG_VERSION=' || return 0
DB_SERVER=$(printenv | grep '_ENV_PG_VERSION=' | sed -e 's/_ENV_PG_VERSION=.*//' | tr '[:upper:]' '[:lower:]' || true)
DB_PORT=$(printenv | grep "^$(echo $DB_SERVER | tr '[:lower:]' '[:upper:]')_PORT=" | cut -f2- -d= | rev | cut -f1 -d: | rev || true)
DB_USER=$(printenv | grep "^$(echo $DB_SERVER | tr '[:lower:]' '[:upper:]')_ENV_POSTGRES_USER"=| cut -f2- -d= || true)
DB_PASSWORD=$(printenv | grep "^$(echo $DB_SERVER | tr '[:lower:]' '[:upper:]')_ENV_POSTGRES_PASSWORD="| cut -f2- -d= || true )
DB_NAME=$(printenv | grep "^$(echo $DB_SERVER | tr '[:lower:]' '[:upper:]')_ENV_DASHBOARD_DB=" | cut -f2- -d= || true)
DB_SERVER=${DB_SERVER:-postgres}
DB_PORT=${DB_PORT:-5432}
DB_USER=${DB_USER:-postgres}
DB_PASSWORD=${DB_PASSWORD:-postgres}
DB_NAME=${DB_NAME:-dashboard_$(echo $RAILS_ENV | tr '[:upper:]' '[:lower:]')}
export DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_SERVER}:${DB_PORT}/${DB_NAME}?pool=${RAILS_MAX_THREADS:-10}"
}
can_load_schema() {
bundle exec rake db:check_protected_environments >/dev/null 2>&1
}
# prepare database
prepare_database() {
bundle exec rake db:create || true
can_load_schema && bundle exec rake db:schema:load || true
bundle exec rake db:migrate || true
bundle exec rake db:seed || true
}
# precompile assets (js,css,images)
prepare_assets() {
bundle exec rake assets:precompile
}
cpu_count() {
grep -c processor /proc/cpuinfo
}
set_worker_count() {
export WEB_CONCURRENCY=$(cpu_count)
export RAILS_MAX_THREADS=$(( WEB_CONCURRENCY * 4 ))
[[ $RAILS_MAX_THREADS -ge 5 ]] || export RAILS_MAX_THREADS=5
}
app_is_running() {
test -S /app/tmp/sockets/puma.sock
}
# wait for app server socket to appear, then start the frontend webserver
start_nginx() {
echo "Starting Nginx" > /proc/1/fd/2
nginx -g 'daemon on;'
}
# wait for app server socket to appear, then start the frontend webserver
start_placeholder() {
bundle exec ruby << '_site_builder'
TITLE=(ENV["LOADER_TITLE"]||"Rails application")
require "pathname"
require "nokogiri"
require "erb"
module Rails; def self.version; Gem::Specification.find_by_name("rails").version.to_s; end; end
site = Nokogiri::XML.parse(ERB.new(File.read(Pathname.new(Gem::Specification.find_by_name("railties").gem_dir).join("lib","rails","templates","rails","welcome","index.html.erb"))).result(binding))
site.xpath("//h1").first.swap("<h1>#{TITLE} is loading ...</h1>")
site.xpath("//title").after("<meta http-equiv='refresh' content='5'>")
File.write("/tmp/loader.html",site.to_s)
_site_builder
ruby <<'_loader'
puts "Starting Loading-Page"
$stdout.reopen("/dev/null","w")
$stderr.reopen("/dev/null","w")
require "securerandom"
ENV["LOADER_SECRET"]||=SecureRandom.hex(16)
server = fork do
require "sinatra"
site=File.read("/tmp/loader.html")
File.unlink("/tmp/loader.html")
set(:server, "webrick")
get("/.loader/is-reachable/#{ENV["LOADER_SECRET"]}"){ENV["LOADER_SECRET"]}
get("/*"){site}
end
fork do
require "net/http"
loop do
sleep 1
response = Net::HTTP.new("localhost",port=(ENV["RAILS_PORT"]||80)).get("/.loader/is-reachable/#{ENV["LOADER_SECRET"]}") rescue nil
next if response.nil?
next if response.code.to_i >= 500
break if response.code != '200'
next if response.body.strip == ENV["LOADER_SECRET"]
end
Net::HTTP.new("localhost",port=(ENV["RAILS_PORT"]||80)).get("/")
sleep 2
`kill #{$$} #{server}`
end
_loader
}
# start the application server
start_puma() {
exec gosu ${RAILS_USER} bundle exec puma --environment $RAILS_ENV --bind unix:///app/tmp/sockets/puma.sock
}
has_docker_socket(){
test -S /var/run/docker.sock
}
allow_docker_access() {
docker_gid=$(stat -c %g /var/run/docker.sock)
addgroup --§ --gid=$docker_gid docker || true
docker_group=$(stat -c %G /var/run/docker.sock)
adduser ${RAILS_USER} $docker_group
}
create_user() {
adduser --system --home=/app/tmp --shell=/bin/bash --group ${RAILS_USER}
}
set_permissions() {
chown -R ${RAILS_USER} /app/tmp
chown -R ${RAILS_USER} /app/log
}
# put it all together
main() {
use_postgresql
if [[ -z "$APP_IS_RUNNING" ]]; then
[[ -n "$@" ]] || start_placeholder
[[ -n "$@" ]] || start_nginx
create_user
has_docker_socket \
&& allow_docker_access \
|| true
set_permissions
set_worker_count
prepare_fs
prepare_secret
prepare_database
if [[ -z "$@" ]]; then
prepare_assets
start_puma
fi
fi
case "$1" in
db:migrate|db:create|db:schema:load|console|server)
set -- rails "$@"
;;
*)
set -- "$@"
;;
esac
exec "$@"
}
vars
main "$@"
_docker_entrypoint
}
cat <<'_Dockerfile' | sed -e "s#@@ENTRYPOINT_B64@@#$(_docker_entrypoint | base64)#g" > Dockerfile
FROM ruby
ARG TINI_VERSION=v0.13.0
RUN curl -sL https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini \
| install -m 0755 -o root -g root /dev/stdin /bin/tini
ARG GOSU_VERSION=1.10
RUN curl -sL https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-amd64 \
| install -m 0755 -o root -g root /dev/stdin /bin/gosu
ARG NODEJS_VERSION=v7.2.0
RUN curl -sL https://nodejs.org/dist/${NODEJS_VERSION}/node-${NODEJS_VERSION}-linux-x64.tar.xz \
| tar -J -x -C /usr/local --strip-components=1
ENV RAILS_LOG_TO_STDOUT=true
ENV RAILS_ENV=production
RUN export DEBIAN_FRONTEND=noninteractive \
&& export DEBIAN_RELEASE=$(grep "debian.org" /etc/apt/sources.list | head -1 | awk '{print $3}') \
&& echo deb http://nginx.org/packages/mainline/debian/ $DEBIAN_RELEASE nginx >> /etc/apt/sources.list.d/nginx.list \
&& curl -sL http://nginx.org/keys/nginx_signing.key | apt-key add - \
&& apt-get update \
&& apt-get dist-upgrade -y \
&& apt-get install -y nginx \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
&& find /etc/nginx -name '*.conf' -print -delete
RUN printf 'worker_processes 2;\nevents {\n worker_connections 1024;\n}\nhttp {\nerror_log /proc/1/fd/2;\naccess_log /proc/1/fd/1;\ninclude mime.types;\ndefault_type application/octet-stream;\nsendfile on;\nkeepalive_timeout 65;\ninclude /etc/nginx/conf.d/*.conf;\n}' | install -D -m 0644 -o root -g root /dev/stdin /etc/nginx/nginx.conf
RUN printf 'upstream application { server unix:/app/tmp/sockets/puma.sock fail_timeout=0; server 127.0.0.1:4567 backup; }\nserver { listen 80; listen [::]:80; root /app/public; try_files $uri/index.html $uri @application; location @application { proxy_pass http://application; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; } error_page 500 502 503 504 /500.html; client_max_body_size 4G; keepalive_timeout 10; }' | install -D -m 0644 -o root -g root /dev/stdin /etc/nginx/conf.d/rails.conf
ENTRYPOINT ["application"]
EXPOSE 80/tcp
RUN gem install --no-ri --no-rdoc bundler
ENV BUNDLE_GITHUB__HTTPS=true
RUN printf "\
source 'https://rubygems.org'\n\
gem 'sinatra'\n" \
| tee /tmp/Gemfile \
&& jobs=$(grep -c ^processor /proc/cpuinfo) \
&& cd /tmp \
&& ( until sh -cx "bundle install --jobs=$jobs"; do sleep 1; done ) \
&& rm /tmp/Gemfile*
# ARG RAILS_VERSION="'>=5', '<5.1'"
ARG RAILS_VERSION="github: 'rails'"
RUN printf "\
source 'https://rubygems.org'\n\
gem 'byebug', platform: :mri\n\
gem 'rails', $RAILS_VERSION\n\
gem 'puma'\n\
gem 'sass-rails'\n\
gem 'uglifier'\n\
gem 'coffee-rails'\n\
gem 'jquery-rails'\n\
gem 'turbolinks'\n\
gem 'jbuilder'\n\
gem 'bcrypt'\n\
gem 'versionist'\n\
gem 'dalli'\n\
gem 'bootstrap-sass'\n\
gem 'slim'\n\
gem 'slim-rails'\n" \
| tee /tmp/Gemfile \
&& jobs=$(grep -c ^processor /proc/cpuinfo) \
&& cd /tmp \
&& ( until sh -cx "bundle install --jobs=$jobs"; do sleep 1; done ) \
&& rm /tmp/Gemfile*
WORKDIR /app
ADD Rakefile /app/Rakefile
ADD config.ru /app/config.ru
ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN jobs=$(grep -c ^processor /proc/cpuinfo) \
&& ( until sh -cx "bundle install --jobs=$jobs --without development test"; do sleep 1; done ) \
&& mkdir -p log tmp/pids tmp/sockets
RUN printf '@@ENTRYPOINT_B64@@' | base64 -d | install -D -m 0755 -o root -g root /dev/stdin /bin/application
ADD bin /app/bin
ADD public /app/public
ADD db /app/db
ADD lib /app/lib
ADD config /app/config
ADD app /app/app
_Dockerfile
trap 'rm Dockerfile' exit
if [[ "$1" = "build" ]]; then
shift
docker build --tag="$(whoami)/$(basename $PWD)" "$@"
echo "$0 run --rm --publish=8080:80 --name='$(basename $PWD)' '$(whoami)/$(basename $PWD)'"
else
docker "$@"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment