Skip to content

Instantly share code, notes, and snippets.

@dombarnes
Created January 1, 2024 23:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dombarnes/4bf1d36bea00ce525295c721a0b8d700 to your computer and use it in GitHub Desktop.
Save dombarnes/4bf1d36bea00ce525295c721a0b8d700 to your computer and use it in GitHub Desktop.
My current Rails/Docker/Azure CI pipeline
stages:
- stage: PreflightCheck
displayName: Run basic code checks
jobs:
- job: RubyPreflight
steps:
- task: DockerCompose@0
displayName: Build Image
inputs:
action: Build services
containerregistrytype: "Azure Container Registry"
azureSubscriptionEndpoint: $(azureSubscriptionEndpoint)
azureContainerRegistry: $(azureContainerRegistry)
dockerComposeFile: docker-compose.ci.test.yml
dockerComposeCommand: build
dockerComposeFileArgs: "RAILS_MASTER_KEY=$(TEST_MASTER_KEY)"
qualifyImageNames: true
projectName: $(dockerAppName)
arguments: --build-arg RAILS_MASTER_KEY=$(TEST_MASTER_KEY)
- task: DockerCompose@0
displayName: Run Rspec
continueOnError: true
timeoutInMinutes: 5
env:
BUILD_SRC: $(Build.SourcesDirectory)
inputs:
action: Run a specific service
azureSubscriptionEndpoint: $(azureSubscriptionEndpoint)
azureContainerRegistry: $(azureContainerRegistry)
dockerComposeFile: docker-compose.ci.test.yml
projectName: $(dockerAppName)
qualifyImageNames: true
buildImages: false
serviceName: app
# containerCommand: "bin/rails db:create db:migrate && bin/rspec spec --tag=~type:feature --format RspecJunitFormatter --out test/TEST-rspec.xml"
abortOnContainerExit: true
detached: false
dockerComposeFileArgs: |
BUILD_SRC=$(Build.SourcesDirectory)
- bash: |
if [ -f $(Build.SourcesDirectory)/log/bullet.log ]; then
cat $(Build.SourcesDirectory)/log/bullet.log
else
echo "No Bullet log found"
fi
displayName: Bullet.log
continueOnError: true
- task: PublishTestResults@2
displayName: Publish Rspec Results
condition: succeededOrFailed()
inputs:
testRunner: JUnit
testResultsFiles: "$(Pipeline.Workspace)/**/test-*.xml"
testRunTitle: "Ruby tests"
- task: PublishCodeCoverageResults@2
continueOnError: true
inputs:
summaryFileLocation: "$(Pipeline.Workspace)/**/coverage.xml"
pathToSources: "$(Build.SourcesDirectory)/rails"
- bash: |
echo "Executing docker run command"
docker run --rm --name tests -v $(Build.SourcesDirectory)/test:/app/test -w /app -e RAILS_ENV=test registry.docker.io/my_app:test sh -c "bin/rails zeitwerk:check > /app/test/zeitwerk.log"
displayName: Run zeitwerk:check
continueOnError: true
- bash: |
echo "Executing docker run command"
docker run --rm --name tests -v $(Build.SourcesDirectory)/test:/app/test -w /app -e RAILS_ENV=test registry.docker.io/my_app:test bundle exec brakeman -f junit -o test/test-brakeman-report.xml
displayName: Run Brakeman
continueOnError: true
enabled: true
- task: PublishTestResults@2
displayName: Publish Brakeman Results
condition: succeededOrFailed()
inputs:
testRunner: JUnit
testResultsFiles: "$(Build.SourcesDirectory)/test/test-brakeman-report.xml"
testRunTitle: "Brakeman tests"
- bash: |
echo "Executing docker run command"
docker run --rm --name tests -v $(Build.SourcesDirectory)/test:/app/test -w /app -e RAILS_ENV=test registry.docker.io/my_app:test bundler-audit check --update
displayName: Run Bundler Audit
continueOnError: true
- task: PublishPipelineArtifact@1
displayName: Publish Artifact Staging directory for release pipeline
inputs:
targetPath: "$(Build.SourcesDirectory)/test"
artifactName: "TestOutput"
- stage: BuildDocker
displayName: Build an updated docker image
jobs:
- job: BuildRailsDocker
steps:
- task: DockerCompose@0
displayName: Container registry login
inputs:
containerregistrytype: Azure Container Registry
azureSubscriptionEndpoint: $(azureSubscriptionEndpoint)
azureContainerRegistry: $(azureContainerRegistry)
dockerComposeFile: docker-compose.ci.yml
dockerComposeCommand: "version"
workingDirectory: $(projectDirectory)
- task: DockerCompose@0
displayName: Build Image
inputs:
action: Build services
containerregistrytype: "Azure Container Registry"
azureSubscriptionEndpoint: $(azureSubscriptionEndpoint)
azureContainerRegistry: $(azureContainerRegistry)
dockerComposeFile: docker-compose.ci.yml
dockerComposeCommand: build
qualifyImageNames: true
projectName: $(dockerAppName)
arguments: |
--build-arg RAILS_MASTER_KEY=$(RAILS_MASTER_KEY)
- task: DockerCompose@0
displayName: Test boot Docker image
inputs:
action: Run services
azureSubscriptionEndpoint: $(azureSubscriptionEndpoint)
azureContainerRegistry: $(azureContainerRegistry)
dockerComposeFile: docker-compose.ci.yml
projectName: $(dockerAppName)
qualifyImageNames: true
buildImages: false
abortOnContainerExit: true
detached: false
- task: Docker@2
displayName: Tag Build
# condition: and(succeeded(), eq(variables.isDevelop, 'true'))
inputs:
command: tag
arguments: registry.docker.io/$(dockerImageName):latest registry.docker.io/$(dockerImageName):$(buildName)
- bash: |
echo "##vso[task.setvariable variable=platform_tag;isOutput=true]$(buildName)"
echo "##vso[task.setvariable variable=commit_id;isOutput=true]$(sourceVersion)"
echo "platform_tag to use is $(buildName)"
workingDirectory: $(Build.SourcesDirectory)
name: platformTag
displayName: Set platform_tag to dev
- task: DockerCompose@0
condition: or(eq(variables.canDeploy, 'true'), contains(variables['Build.SourceVersionMessage'], '[uat]'))
displayName: Push image tags to Azure CR
inputs:
action: Push services
azureSubscriptionEndpoint: $(azureDockerServiceConnection)
azureContainerRegistry: $(azureContainerRegistry)
dockerComposeFile: docker-compose.ci.yml
includeLatestTag: false
# imageName: 'registry.docker.io/$(dockerAppName)_app:$(buildName)'
additionalImageTags: $(buildName)
projectName: $(dockerAppName)
#!/usr/bin/env sh
set -e
# Clear PID files
PID_FILE="${PIDFILE:-/tmp/pids/server.pid}"
if [ -f $PID_FILE ]; then
echo "Deleting pidfile"
rm $PID_FILE
else
echo "No pidfile to remove"
echo "Creating folder $PID_FILE"
mkdir -p $( dirname "$PID_FILE")
fi
# Starting sshd process for Kudu
echo "Configuring SSH access via port ${SSH_PORT:=2222}"
sed -i "s/SSH_PORT/$SSH_PORT/g" /etc/ssh/sshd_config
echo "Starting SSH server"
/usr/sbin/sshd
# Get environment variables to show up in SSH session
eval $(printenv | sed -n "s/^\([^=]\+\)=\(.*\)$/export \1=\2/p" | sed 's/"/\\\"/g' | sed '/=/s//="/' | sed 's/$/"/' >> /etc/profile)
# Start Rails
# Switch to the app user. Don't run as root.
echo "Switching to app user"
su - appuser
if [ $CI -ne 'true' ]; then
while ! pg_isready -q -h $DB_HOST -p $DB_PORT -U $DB_USERNAME
do
echo "$(date) - waiting for database to start."
sleep 2
done
fi
if [ $# -ne 0 ]
then
echo "Executing $@"
exec "$@"
else
echo "defaulting to command: \"bundle exec puma -C config/puma.rb -e ${RAILS_ENV} -b tcp://0.0.0.0 -p ${PORT} config.ru"
if [ -n "$WAIT_FOR_HOST" ]; then
echo "Waiting for $WAIT_FOR_HOST:$WAIT_FOR_PORT to be available..."
exec sh -c "entrypoints/wait-for $WAIT_FOR_HOST:$WAIT_FOR_PORT -t 45 -- bundle exec puma -C config/puma.rb -e ${RAILS_ENV} -b tcp://0.0.0.0 -p ${PORT} config.ru"
else
echo "Booting Rails"
exec sh -c "bundle exec puma -C config/puma.rb -e ${RAILS_ENV} -b tcp://0.0.0.0 -p ${PORT} config.ru"
fi
fi
FROM ruby:3.2.2-alpine3.18 as dependencies
ARG RAILS_ENV=production
ARG RAILS_LOG_TO_STDOUT=true
ARG RAILS_SERVE_STATIC_FILES=true
ARG RAILS_MASTER_KEY=''
ARG OAUTH_CLIENT_ID=''
ARG OAUTH_CLIENT_SECRET=''
ARG OAUTH_SERVER=''
ARG PORT=3000
ARG PORTAL_URL=''
ARG TZ='Europe/London'
ARG BUNDLE_WITHOUT="development test"
ENV BUNDLE_WITHOUT=$BUNDLE_WITHOUT CA_CERTS_PATH=/etc/ssl/certs RAILS_ENV=$RAILS_ENV RAILS_LOG_TO_STDOUT=$RAILS_LOG_TO_STDOUT RAILS_SERVE_STATIC_FILES=$RAILS_SERVE_STATIC_FILES OAUTH_SERVER=$OAUTH_SERVER OAUTH_CLIENT_ID=$OAUTH_CLIENT_ID OAUTH_CLIENT_SECRET=$OAUTH_CLIENT_SECRET PORT=$PORT PORTAL_URL=$PORTAL_URL TZ=$TZ
# Install dependencies:
# - build-base: To ensure certain gems can be compiled
# - nodejs: Compile assets
# - postgresql-dev postgresql-client: Communicate with postgres through the postgres gem
# - libxslt-dev libxml2-dev: Nokogiri native dependencies
# - imagemagick: for image processing
ENV NODE_VERSION 18.17.1
RUN apk upgrade --available --no-cache \
&& apk add --no-cache --update \
binutils-gold \
build-base \
busybox \
# ca-certificates \
curl \
# file \
g++ \
gcc \
# for compiling scss
gcompat \
git \
gnupg \
graphicsmagick \
# less \
libc-dev \
# libpq-dev \
# libpq \
libffi-dev \
libsodium-dev \
libstdc++ \
# for Nokogiri
libxml2-dev \
# for Nokogiri
libxslt-dev \
libgcrypt-dev \
linux-headers \
make \
netcat-openbsd \
nodejs-current \
openssh-client \
openssl \
pkgconfig \
postgresql-dev \
# postgresql \
python3 \
readline-dev \
# tzdata \
yarn \
vips-dev \
zlib
ARG APP_PATH=/app
RUN mkdir -p ${APP_PATH}
WORKDIR ${APP_PATH}
# Gem installation
ENV SECRET_KEY_BASE=123456 RAILS_MASTER_KEY=${RAILS_MASTER_KEY} API_AUTH='' API_CLIENT_ID='' API_CLIENT_SECRET='' JWT_HMAC_SHARED_SECRET='' OAUTH_CLIENT_ID='' OAUTH_CLIENT_SECRET='' PIPEDRIVE_API_KEY=''
ENV BUNDLER_VERSION 2.4.19
ENV YARN_VERSION 3.6.4
RUN printf "%s\n" "gem: --no-document" > /usr/local/etc/gemrc \
&& gem update --system \
&& gem install bundler -v $BUNDLER_VERSION --no-document
COPY Gemfile Gemfile.lock .ruby-version Rakefile ${APP_PATH}/
# Fix for system writable tmp directory
RUN chmod +t /tmp
RUN bundle update --bundler \
&& bundle config --global frozen 1 \
&& bundle config build.nokogiri --use-system-libraries \
&& bundle config set without $BUNDLE_WITHOUT \
&& bundle config \
&& bundle check \
|| bundle install --quiet -j4 --retry 3 \
&& bundle clean --force \
&& bundle exec bootsnap precompile --gemfile \
&& rm -rf /usr/local/bundle/bundler/gems/*/.git \
/usr/local/bundle/cache/ \
&& find /usr/local/bundle/ruby/*/extensions \
-type f -name "mkmf.log" -o -name "gem_make.out" | xargs rm -f \
&& find /usr/local/bundle/ruby/*/gems -maxdepth 2 \
\( -type d -name "spec" -o -name "test" -o -name "docs" \) -o \
\( -name "*LICENSE*" -o -name "README*" -o -name "CHANGELOG*" \
-o -name "*.md" -o -name "*.txt" -o -name ".gitignore" \
-o -name ".rubocop.yml" -o -name ".yardopts" -o -name ".rspec" \
-o -name "COPYING" -o -name "SECURITY" \
-o -name "HISTORY" -o -name "CODE_OF_CONDUCT" -o -name "CONTRIBUTING" \
\) | xargs rm -rf \
&& find /usr/local/bundle/gems -name "*.c" -delete && \
find /usr/local/bundle/gems -name "*.h" -delete && \
find /usr/local/bundle/gems -name "*.o" -delete && \
find /usr/local/bundle/gems -name "*.html" -delete
COPY package.json yarn.lock .yarnrc.yml ${APP_PATH}/
COPY .yarn ${APP_PATH}/.yarn
RUN corepack enable && yarn install --immutable
# Copy all files to /app (except what is defined in .dockerignore)
ENV JWT_HMAC_SHARED_SECRET=''
ENV REDIS_URL=''
RUN mkdir -p ${APP_PATH}/log \
&& rake tmp:create \
&& rake log:clear \
&& mkdir -p ${APP_PATH}/public
COPY . ${APP_PATH}
RUN bundle exec rails zeitwerk:check \
&& bundle exec bootsnap precompile app/ lib/
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
############### Build step done ###############
FROM ruby:3.2.2-alpine3.18
ARG RAILS_ENV=production
ARG AIRBRAKE_KEY=''
ARG AIRBRAKE_PROJECT_ID=''
ARG PORT=3000
ARG RAILS_LOG_TO_STDOUT=true
ARG RAILS_SERVE_STATIC_FILES=true
ARG APP_PATH=/app
ARG APP_USER=appuser
ENV PORT=$PORT RAILS_ENV=$RAILS_ENV AIRBRAKE_KEY=$AIRBRAKE_KEY AIRBRAKE_PROJECT_ID=$AIRBRAKE_PROJECT_ID PIPEDRIVE_API_KEY=${PIPEDRIVE_API_KEY}
# Set ENV keys needed to start up app
ENV API_AUTH='' API_CLIENT_ID='' API_CLIENT_SECRET='' JWT_HMAC_SHARED_SECRET='' OAUTH_CLIENT_ID='' OAUTH_CLIENT_SECRET=''
ENV CA_CERTS_PATH=/etc/ssl/certs RAILS_ENV=${RAILS_ENV} RAILS_LOG_TO_STDOUT=${RAILS_LOG_TO_STDOUT} RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} PORT=${PORT}
# Installed required packages
RUN apk upgrade --available --no-cache \
&& apk add --no-cache --update \
brotli-libs \
busybox \
ca-certificates \
curl \
curl \
file \
# gcompat \
git \
gnupg \
graphicsmagick \
libpq \
libsodium-dev \
libxml2 \
libxslt \
nano \
# openssh \
openssh-server \
postgresql-client \
tcptraceroute \
tzdata \
vips \
yaml \
&& rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Set the password apk upgrade --available ot to "Docker!". In this example, "apk add" is the install instruction for an Alpine Linux-based image.
RUN echo "root:Docker!" | chpasswd
# Create a group and user
RUN addgroup -S appgroup \
&& adduser -S $APP_USER -G appgroup -s /bin/sh -h /${APP_PATH} \
# && mkdir -p ${APP_PATH} \
&& chown -R ${APP_USER}:root "$APP_PATH" db log storage tmp \
# && chmod 700 "$APP_PATH" \
&& chmod 777 /tmp/ \
&& chown -R ${APP_USER}:root /tmp/
# Tell docker that all future commands should run as the appuser user
# USER $APP_USER
WORKDIR ${APP_PATH}
RUN bundle config --local without $BUNDLE_WITHOUT
# Azure Kudu support
COPY ssh_setup.sh /tmp
COPY entrypoints/mymotd.sh /etc/profile.d/
COPY --from=dependencies /usr/local/bundle/ /usr/local/bundle/
COPY --from=dependencies --chown=${APP_USER}:appgroup ${APP_PATH}/ ${APP_PATH}/
# Run SSH config for Azure Kudu
RUN chmod +x /etc/profile.d/mymotd.sh \
&& chmod -R +x /tmp/ssh_setup.sh \
&& (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null) \
&& rm -rf /tmp/* \
&& mkdir -p ${APP_PATH}/tmp/pids /tmp/pids
COPY sshd_config /etc/ssh/
COPY bashrc /root/.bashrc
ENV SSH_PORT=2222
EXPOSE ${SSH_PORT} ${PORT}
ENTRYPOINT [ "./entrypoints/docker-entrypoint.sh" ]
FROM ruby:3.2.2-alpine3.18
ARG RAILS_ENV=test
ARG RAILS_LOG_TO_STDOUT=true
ARG AIRBRAKE_KEY=''
ARG AIRBRAKE_PROJECT_ID=''
ARG RAILS_SERVE_STATIC_FILES=true
ARG RAILS_MASTER_KEY=''
ARG OAUTH_CLIENT_ID=''
ARG OAUTH_CLIENT_SECRET=''
ARG OAUTH_SERVER=''
ARG PORT=3000
ARG PORTAL_URL=''
ARG TZ='Europe/London'
# OS Level ENV VARS
ENV CA_CERTS_PATH=/etc/ssl/certs TZ=$TZ
# Install dependencies:
# - build-base: To ensure certain gems can be compiled
# - nodejs: Compile assets
# - postgresql-dev postgresql-client: Communicate with postgres through the postgres gem
# - libxslt-dev libxml2-dev: Nokogiri native dependencies
# - imagemagick: for image processing
# RUN apk --update add build-base nodejs tzdata postgresql-dev postgresql-client libxslt-dev libxml2-dev imagemagick
RUN apk update \
&& apk upgrade \
&& apk add --no-cache --update \
build-base \
# ca-certificates \
# file \
# g++ \
# gcc \
# gcompat \
git \
# gnupg \
graphicsmagick \
# less \
# libc-dev \
# libpq-dev \
# libffi-dev \
# libsodium-dev \
# libstdc++ \
libxml2-dev \
libxslt-dev \
# libgcrypt-dev \
# linux-headers \
# make \
# netcat-openbsd \
nodejs \
openssl \
pkgconfig \
postgresql-dev \
postgresql \
python3 \
tzdata \
vips-dev \
yarn
ARG APP_PATH=/app
RUN mkdir -p ${APP_PATH}
WORKDIR ${APP_PATH}
# Framework level env vars
ENV RAILS_ENV=$RAILS_ENV RAILS_LOG_TO_STDOUT=$RAILS_LOG_TO_STDOUT RAILS_SERVE_STATIC_FILES=$RAILS_SERVE_STATIC_FILES PORT=$PORT RAILS_MASTER_KEY=${RAILS_MASTER_KEY} AIRBRAKE_KEY=$AIRBRAKE_KEY AIRBRAKE_PROJECT_ID=$AIRBRAKE_PROJECT_ID
# Gem installation
ENV SECRET_KEY_BASE_DUMMY=1 SECRET_KEY_BASE=123456 API_AUTH='' API_CLIENT_ID='' API_CLIENT_SECRET='' JWT_HMAC_SHARED_SECRET='' PIPEDRIVE_API_KEY='' OAUTH_SERVER=$OAUTH_SERVER OAUTH_CLIENT_ID=$OAUTH_CLIENT_ID OAUTH_CLIENT_SECRET=$OAUTH_CLIENT_SECRET PORTAL_URL=$PORTAL_URL REDIS_URL=''
ENV DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL=true
ENV BUNDLER_VERSION=2.4.19
RUN gem update --system \
&& gem install bundler -v $BUNDLER_VERSION --no-document
COPY Gemfile.lock Gemfile Rakefile .ruby-version ${APP_PATH}
COPY . ${APP_PATH}
# Fix for system writable tmp directory
RUN chmod +t /tmp
RUN bundle update --bundler \
&& bundle config --global frozen 1 \
&& bundle config build.nokogiri --use-system-libraries \
&& bundle config set without 'development' \
&& bundle config \
&& bundle check \
|| bundle install -j4 --retry 3 \
&& bin/rails zeitwerk:check
ARG APP_PATH=/app
ARG APP_USER=appuser
RUN addgroup -S appgroup \
&& adduser -S ${APP_USER} -G appgroup -s /bin/sh -h /home/appuser \
&& mkdir -p ${APP_PATH} \
&& chown -R ${APP_USER} "$APP_PATH" \
&& chmod 700 "$APP_PATH" \
&& chmod 777 /tmp/ \
&& chown $APP_USER:root /tmp/
RUN chmod +t /tmp
# COPY package.json yarn.lock ${APP_PATH}/
# RUN yarn install --check-files --frozen-lockfile --production
# Copy all files to /app (except what is defined in .dockerignore)
# COPY . ${APP_PATH}
RUN mkdir -p ${APP_PATH}/log \
&& rake tmp:create \
&& rake log:clear \
&& mkdir -p ${APP_PATH}/public
# Set the password for root to "Docker!". In this example, "apk add" is the install instruction for an Alpine Linux-based image.
RUN bundle config --local with 'test'
ENTRYPOINT [ "./entrypoints/rspec-entrypoint.sh" ]
#!/usr/bin/env sh
set -e
eval $(printenv | sed -n "s/^\([^=]\+\)=\(.*\)$/export \1=\2/p" | sed 's/"/\\\"/g' | sed '/=/s//="/' | sed 's/$/"/' >> /etc/profile)
echo "Switching to app user"
su - appuser
echo "Setting up database"
bin/rails db:create
if [ $# -ne 0 ]
then
echo "Executing $@"
exec "$@"
else
echo "Runnng rspec"
exec sh -c "bin/rspec spec --tag=~type:feature --format RspecJunitFormatter --out test/TEST-rspec.xml"
fi
@dombarnes
Copy link
Author

All without comments and context. Hopefully you have some understanding of the tools.
My Dockerfile has some extra config needed to run in Azure's App Service environments. Remove this if you're hosting elsewhere.

I bundle this all up with a docker-compose template that I use sed to substitute in the actual build tag (which is release-$(someIdentifier) - in my case I use our CI build ID as its then easy to track back through CI to the git commit). Then update Azure with that in my deploy process. Throw in some git branch checks for main to make sure we only publish the release branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment