40
loading...
This website collects cookies to deliver better user experience
apt
package manager) can be around 1000Mb. To go down to a slim size of approx 200 Mb image, you may:apk
package manager)docker-compose
.BUNDLE_PATH='vendor/bundle'
This also indicates to Bundler where the gems are located, so it is repeated in the second stage.
bundle config set --without 'development test'
rails new <app-name> --skip-sprockets ...
In particular, this removes the sass-rails
gem which besides saving space, reduces a lot building time.
RUN bundle exec rails webpacker:compile
RUN
commands together since every new layer will increase the final size, so the less the better.RAILS_LOG_TO_STDOUT=true
maxmemory 100mb
(set in "redis.conf" circa line 566) and a maxmemory policy maxmemory-policy allkeys-lru
. We will load the gem hiredis
, a C client library designed for speeding up, and configure:#/config/production.rb
config.cache_store = :redis_cache_store, { driver: :hiredis, url: ENV['REDIS_CACHE'] }
ENV['REDIS_CACHE']
is a reference to the dedicated Redis cache server URL (different from the one Sidekiq uses).The mandatory tzdata
package is needed by the tzinfo-data
gem when running on Windows.
Since we use Postgres, we still need the libpq
package so Rails can communicate with Postgres.
#alpine.prod.Dockerfile
ARG RUBY_VERSION
FROM ruby:${RUBY_VERSION} AS builder
ARG BUNDLER_VERSION
ARG NODE_ENV
ARG RAILS_ENV
ARG RAILS_LOG_TO_STDOUT
RUN apk -U upgrade && apk add --no-cache \
postgresql-dev nodejs yarn build-base
WORKDIR /app
COPY Gemfile Gemfile.lock package.json yarn.lock ./
ENV LANG=C.UTF-8 \
BUNDLE_JOBS=4 \
BUNDLE_RETRY=3 \
BUNDLE_PATH='vendor/bundle'
RUN gem install bundler:${BUNDLER_VERSION} --no-document \
&& bundle config set --without 'development test' \
&& bundle install --quiet \
&& rm -rf $GEM_HOME/cache/* \
&& yarn --check-files --silent --production && yarn cache clean
COPY . ./
RUN bundle exec rails webpacker:compile assets:clean
############################################################
FROM ruby:${RUBY_VERSION}
ARG RAILS_ENV
ARG RAILS_LOG_TO_STDOUT
RUN apk -U upgrade && apk add --no-cache libpq netcat-openbsd tzdata\
&& rm -rf /var/cache/apk/* \
&& adduser --disabled-password app-user
# --disabled-password: don't assign a pwd, so cannot login
USER app-user
COPY --from=builder --chown=app-user /app /app
ENV RAILS_ENV=$RAILS_ENV \
BUNDLE_PATH='vendor/bundle' \
RAILS_LOG_TO_STDOUT=$RAILS_LOG_TO_STDOUT
WORKDIR /app
RUN rm -rf node_modules
docker build -t usr/appname --build-arg RUBY_VERSION=3.0.1-alpine --build-arg NODE_ENV=production --build-arg RAILS_ENV=production --build-arg BUNDLER_VERSION=2.2.21 --build-arg RAILS_SERVE_STATIC_FILES=true -f _alpine.prod.Dockerfile .
docker push usr/appname
docker build --build-arg RAILS_ENV=production ...
or from the "docker-compose". They are just declared in the Dockerfile with ARG RAILS_ENV
(only supports one arg).RAILS_ENV=production
that are already args, then we can pass the "arg" value to the "env" value with:ENV RAILS_ENV=$RAILS_ENV
${RAILS_ENV:-production}
, then this supplies a default value.webpacker-dev-server
and code bindings to accelerate the changes. Size is not the priority, but when using Alpine and Webpack only, the compilation runs faster. It is largely inspired by these guys and this guy but somehow simplified. ARG RUBY_VERSION
FROM ruby:${RUBY_VERSION} AS builder
ARG BUNDLER_VERSION
ARG NODE_ENV
ARG RAILS_ENV
ENV RAILS_ENV=${RAILS_ENV:-development} \
NODE_ENV=${NODE_ENV:-development} \
BUNDLER_VERSION=${BUNDLER_VERSION:-2.2.21}
RUN apk update && apk add --no-cache \
build-base postgresql-dev nodejs yarn \
tzdata netcat-openbsd \
&& rm -rf /var/cache/apk/*
WORKDIR /app
COPY Gemfile Gemfile.lock package.json yarn.lock ./
ENV LANG=C.UTF-8 \
BUNDLE_JOBS=4 \
BUNDLE_RETRY=3
# BUNDLE_PATH='vendor/bundle'
# <- to bundle only the gems needed from Gemfile into local folder /vendor/bundle
RUN gem install bundler:${BUNDLER_VERSION} --no-document \
&& bundle install --quiet \
&& rm -rf /usr/local/bundle/cache/*.gem \
&& find /usr/local/bundle/gems/ -name "*.c" -delete \
&& find /usr/local/bundle/gems/ -name "*.o" -delete
RUN yarn --check-files --silent && yarn cache clean
COPY . ./
overmind start
with the following "Procfile":#Procfile
assets: ./bin/webpack-dev-server
web: bundle exec rails server
redis-server: redis-server redis/redis.conf
worker: bundle exec sidekiq -C config/sidekiq.yml
ENV['REDIS_URL']
in the code for both Sidekiq and Redis (see note at the end). We used for example a free tier from Redislabs.depends_on:
keys) and use the remote url. For example, we can use a free tier service ElephantSQL and set ENV['ELEPHANT_URL']
pointing to the managed service.bundle exec rails server
with an open port,bundle exec sidekiq
with a config file to link to the Redis session used.redis-server
and uses a config file (password, persistence)...).PG_DATA=/var/lib/postgresql/data
) and Redis (/data
).the "sidekiq" service is initialised in the file "/config/initialisers/sidekiq.rb" where we provide the link to a Redis session via ENV['SIDEKIQ_REDIS']
.
the "app" service initialises the Redis in-memory database in a ad-hoc "/config.redis.yml" file where we pass ENV['REDIS_URL']
.
It is initialised with a "redis.conf" file that holds the password requirepass secretpwd
(circa line 500).
Since "sidekiq" and "app" are built with the same image, the app code holds the environment variables REDIS_URL
and SIDEKIQ_REDIS
in the ".env" file. This ".env" file is passed to the "app", "sidekiq" and "redis" services.
rake db:prepare
to create and migrate the database. Again, we need to pass the same credentials with the environment variable POSTGRES_PASSWORD=<password>
in the docker-compose.yml file and save them in the ".env" file in the code (see note at the end).version: "3"
x-app: &common
env_file: .env
build:
context: .
dockerfile: alpine.prod.Dockerfile
args:
- RUBY_VERSION=3.0.1-alpine
- BUNDLER_VERSION=2.2.21
- NODE_ENV=production
- RAILS_ENV=production
services:
pg:
image: postgres:13.3-alpine
ports:
- 5432
environment:
- POSTGRES_PASSWORD=dockerpassword
volumes:
- pg_data:/var/lib/postgresql/data
- ./pg/init-user.sql:/docker-entrypoint-initdb.d/init-user.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
redisdb:
build:
context: ./redis
ports:
- 6379
env_file: .env
command: ["redis-server", "/usr/local/etc/redis.conf"]
volumes:
- redis_data:/data
- ./redis/config:/usr/local/etc/redis:ro
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
sidekiq:
<<: *common
entrypoint: ["./app-pid.sh"]
command: ["bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml"]
depends_on:
redisdb:
condition: service_health
app:
<<: *common
depends_on:
pg:
condition: service_healthy
redisdb:
condition: service_healthy
ports:
- 4000:3000
entrypoint: [./manage-db.sh]
command: ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
tmpfs:
- /tmp
volumes:
redis_data: {}
pg_data: {}
docker-compose up --build
docker-compose run --rm app bundle exec rails db:prepare
We added the package netcat
in the Dockerfile since we use nc
for health testing. This is for testing purposes and should be removed when using a managed service.
webpacker-dev-server
for hot-reload,version: "3"
x-app: &common
env_file: .env
build:
context: .
args:
- RUBY_VERSION=3.0.1-alpine
- BUNDLER_VERSION=2.2.21
#- NODE_VERSION=14 <- for "slim-buster" based image
- NODE_ENV=development
- RAILS_ENV=development
services:
pg:
image: postgres:13.3-alpine
ports:
- 5432
environment:
- POSTGRES_PASSWORD=cyberdyne
- PG_DATA=/var/lib/postgresql/data
volumes:
- pg_data:/var/lib/postgresql/data
- ./pg/init-user.sql:/docker-entrypoint-initdb.d/init-user.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redisdb:
build:
context: ./redis
ports:
- 6379
env_file: .env
command: ["redis-server", "/usr/local/etc/redis.conf"]
volumes:
- redis_data:/data
- ./redis/config:/usr/local/etc/redis:ro
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
webpacker:
<<: *common
command: ["./bin/webpack-dev-server"]
ports:
- "3035:3035"
volumes:
- .:/app:cached
- packs:/app/public/packs
environment:
- WEBPACKER_DEV_SERVER_HOST=0.0.0.0
sidekiq:
<<: *common
entrypoint: ["./app-pid.sh"]
command: ["bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml"]
depends_on:
redisdb:
condition: service_healthy
app:
<<: *common
depends_on:
pg:
condition: service_healthy
redisdb:
condition: service_healthy
environment:
YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
WEBPACKER_DEV_SERVER_HOST: webpacker
ports:
- 4000:3000
entrypoint: ["./manage-db.sh"]
command: ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
volumes:
- bundle_cache:/usr/local/bundle
- node_modules:/app/node_modules
- .:/app:cached
- packs:/app/public/packs
- rails_cache:/app/tmp/cache
tmpfs:
- /tmp
volumes:
redis_data: {}
pg_data: {}
node_modules:
packs:
bundle_cache:
rails_cache:
ARG RUBY_VERSION
FROM ruby:${RUBY_VERSION} AS builder
ARG BUNDLER_VERSION \
NODE_VERSION \
NODE_ENV \
RAILS_ENV
ENV RAILS_ENV=${RAILS_ENV} \
NODE_ENV=${NODE_ENV} \
BUNDLER_VERSION=${BUNDLER_VERSION} \
DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
# for gems to be compiled
build-essential \
# to get desired node version
curl \
&& apt-get clean \
&& rm -rf /var/cache/apt/archives/* \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& truncate -s 0 /var/log/*log
RUN curl -sL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& echo 'deb https://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
# comm with PG with gem 'pg'
libpq-dev \
# compile assets
nodejs \
yarn \
&& apt-get clean \
&& rm -rf /var/cache/apt/archives/* \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& truncate -s 0 /var/log/*log
WORKDIR /app
COPY Gemfile Gemfile.lock package.json yarn.lock ./
ENV LANG=C.UTF-8 \
BUNDLE_JOBS=4 \
BUNDLE_RETRY=3 \
BUNDLE_PATH='vendor/bundle'
RUN gem install bundler:${BUNDLER_VERSION} --no-document \
&& bundle config set --without 'development test' \
&& bundle install --quiet \
&& rm -rf /usr/local/bundle/cache/*.gem \
&& find /usr/local/bundle/gems/ -name "*.c" -delete \
&& find /usr/local/bundle/gems/ -name "*.o" -delete
RUN yarn --check-files --silent
COPY . ./
RUN bundle exec rails webpacker:compile
# && rm -rf node_modules tmp/cache app/assets vendor/assets lib/assets spec
###########################################
ARG RUBY_VERSION
FROM ruby:${RUBY_VERSION}
ARG NODE_ENV
ARG RAILS_ENV
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
# detect when services inside containers are up and running
netcat-openbsd \
# communicate with PG with gem 'pg'
libpq-dev \
&& apt-get clean \
&& rm -rf /var/cache/apt/archives/* \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& truncate -s 0 /var/log/*log
#<- if didn't used the flag BUNBLER_PATH='vendor/bundle', we would copy the host bundle folder
# COPY --from=builder /usr/local/bundle/ /usr/local/bundle/
RUN adduser --disabled-password app-user
USER app-user
COPY --from=builder --chown=app-user /app /app
ENTRYPOINT ["./app-pid.sh"]
ENV RAILS_ENV=$RAILS_ENV\
NODE_ENV=$NODE_ENV \
RAILS_LOG_TO_STDOUT=true \
RAILS_SERVE_STATIC_FILES=true \
BUNDLE_PATH='vendor/bundle'
WORKDIR /app
RUN rm -rf node_modules tmp/cache lib/assets
EXPOSE 3000
If you want to rely on a remote Postgres or Redis service, you need to configure your account and pass the provided URL to the app.
ENV[ELEPHANT_URL]
into "/config/database.yml" and set the supplied POSTGRES_PASSWORD=xxxx
variable in the "docker-compose.yml" file.a local "containerised" Redis server for Sidekiq's queue. Set SIDEKIQ_REDIS=redis://user:password@redisdb:6379
in the file "/config/sidekiq.rb"
a remote Redis database for the app. We used a (free) service supplied by Redis Labs. Set url: <%= ENV.fetch('REDIS_URL','') %>
with the supplied URL from Redislabs in the file "/config/redis.yml". This can replace the Redis service.
#.env
export POSTGRES_URL=postgresql://docker:dockerpassword@pg:5432
export ELEPHANT_URL=postgres://ortkcbqt:fhSBQrF3Dzl9WWA1FfRIjQmU7u3pBtTd@batyr.db.elephantsql.com/ortkcbqt
export SIDEKIQ_REDIS=redis://user:secretpwd@redisdb:6379
export REDIS_URL=redis://user:tq4hBlYvIvq0uU7hYMOYS6ErQKsSA2N8@redis-13424.c258.us-east-1-4.ec2.cloud.redislabs.com:13424
export RAILS_MASTER_KEY=cce3c51968fc41dd85b3d8b5d54f43eb
export RAILS_SERVE_STATIC_FILES=true
export RAILS_LOG_TO_STDOUT=true
#init.sql
DO $$
BEGIN
CREATE ROLE <docker> WITH SUPERUSER CREATEDB LOGIN PASSWORD <'dockerpassword'>;
EXCEPTION WHEN DUPLICATE_OBJECT THEN
RAISE NOTICE 'not creating role my_role -- it already exists';
END
$$;
POSTGRES_URL=postgresql://<user>:<password>@pg:5423
passed to the "database.yml" in the code must match the "|" used in the "init-user.sql" file and we must pass POSTGRES_PASSWORD=<pasword>
in the "docker-compose.yml" file.docker volume prune
and rebuild.rails db:prepare
is idempotent.#manage-db.sh <-- "app" service
#!/bin/sh
set -e
if [ -f tmp/pids/server.pid ]; then
rm tmp/pids/server.pid
fi
echo "Waiting for Postgres to start..."
while ! nc -z pg 5432; do sleep 0.2; done
echo "Postgres is up"
bundle exec rake db:prepare
exec "$@"
#app-pid.sh <-- "sidekiq" service
#!/bin/sh
set -e
if [ -f tmp/pids/server.pid ]; then
rm tmp/pids/server.pid
fi
exec "$@"
#config/initialisers/redis.rb
REDIS = Redis.new(Rails.application.config_for(:redis))
config_for
will parse and fetch the "/config/redis.yml" config so we can use for example REDIS.set("key", 10)
in the app.> docker-compose run --rm redisdb redis-cli -h redisdb -p 6379
redisdb:6379> get compteur
(error) NOAUTH Authentication required.
redisdb:6379> auth secretpwd
OK
redisdb:6379> get compteur
"3"
dbfilename "dump.rdb"
(line 327)appendonly yes
(line 699) and appendfsync everysec
(line 729) and appendfilename appendonly.aof
(line 703)