How to use Docker for updating Old Production Applications?
Date Published
Reading time
6 min
In this article
- Step 1: Setup
- Step 2: Unify File Configuration
- Step 3: Run Dockerfile
- Step 4: Install NPM and Node.js via Node Version Manager
- Step 5: Containerize the App
- Step 6: Mount Bundled Gems Folder Inside Container
- Step 7: Install Node Packages
- Step 8: Create docker-compose.yml File
- Step 9: Start and Run the Application
- Step 10: Prepare to Release the App to Production
When working on continuously expanding projects over long periods of time, software developers often come across code that must be updated in order for the project to progress. If the code is outdated, preparing the environment and launching certain applications become problematic. Docker is an open-source, virtual environment tool primarily used on Linux and OS6 applications that allows the developer to more quickly update and deploy apps inside various virtual software containers. These containers do not require separate operating systems. They access the Linux kernel’s features, meaning several containers can be run simultaneously by a single server.
Docker further speeds up deployment by providing consistent development and production environments. All members of the team can use the same system libraries, language run-times, and OS. It supports multiple language versions, including Python, Ruby, Java, and Node, meaning developers can run a script inside a Docker image, rather than installing various languages and running scripts separately. Docker’s “Copy-on-Write” mechanism duplicates any requested code in a standard UNIX pattern. Developers can also begin running a container in less than 0.1 seconds, and the entire container requires less than 1 MB of disc-space. These are significant improvements over standard VMs. To help developers take advantage of these many benefits, this article shows how to update an old production application with Docker. The app uses a non-standard Sphinx build, wkhtmltopdf, NPM, and Resque. They will initially be kept within the application and then moved to their own containers. The goals are to:
- Keep 100% forward compatibility with devs not using Docker.
- Make the app use environment variables for configuration.
- Build Sphinx, install Node.js, and wkhtmltopdf in a basic container.
- Keep the app and Resque in the same container.
- Simplify the Procfile.
- Create a base Docker-composed configuration file and extend it for development and CI envs.
Step 1: Setup
Here is what you’ll need to install Docker and its NFS plug-in for OSX Native, OSX Brew and VirtualBox, or Ubuntu.
For OSX Native:
https://docs.docker.com/docker-for-mac/
For OSX via Brew and VirtualBox:
brew install caskroom/cask/brew-caskbrew cask install virtualboxbrew install docker docker-machine docker-compose# create the vmdocker-machine create -d virtualbox default# import environment variables for the docker-clieval "$(docker-machine env default)"
For Ubuntu:
(
install docker
) (
install docker-compose
) Now that you have Docker installed, you can run the application on it. Docker apps are configured via a Dockerfile, which defines how the container is built.
Step 2: Unify File Configuration
Assume the app you are updating has hard-coded config files under the version control. When running the app in Docker, the secrets will no longer be stored inside the image. When modifying this file to unify the configuration,
Hash#fetch
helps return default value unless it already exists in the ENV variable:
# database.ymldevelopment: &defaultusername: <%= ENV.fetch('MYSQL_USER', 'foo') %>password: <%= ENV.fetch('MYSQL_PASSWORD', 'bar') %>host: <%= ENV.fetch('MYSQL_HOST', '127.0.0.1') %>database: app_development.....# mongoid.ymldevelopment:sessions:default:database: app_developmenthosts:- <%= ENV.fetch('MONGO_HOST', 'localhost') %>:ENV.fetch('MONGO_PORT', 27017) %>...# sphinx.ymldevelopment: &defaultaddress: <%= ENV.fetch('SPHINX_HOST', 'localhost') %>...
Step 3: Run Dockerfile
This app uses light-weighted Ruby: 2.1 as the base image and changes
BUNDLE_PATH
to get cache via volume (https://docs.docker.com/engine/reference/builder):
FROM ruby:2.1-slimRUN apt-get update -qq && \apt-get install -y \build-essential git \libmysqlclient-dev mysql-client libxslt-dev libxml2-dev \curl net-tools --no-install-recommends
Step 4: Install NPM and Node.js via Node Version Manager
At this point, you are explicitly specifying the Node libraries. As mentioned, the application is using a non-standard Sphinx build. Therefore, you can’t use already existing Docker images for this purpose:
ENV NVM_DIR "/root/.nvm"ENV NVM_VERSION 0.33.0ENV NODE_VERSION 7.5.0RUN curl https://raw.githubusercontent.com/creationix/nvm/v${NVM_VERSION}/install.sh | bash \&& . $NVM_DIR/nvm.sh \&& nvm install $NODE_VERSION \&& nvm alias default $NODE_VERSION \&& nvm use defaultENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modulesENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
Step 5: Containerize the App
This step is a temporary solution to keep Sphinx within the application. When you are preparing for production release, Sphinx must be moved to a separate container:
WORKDIR /tmpRUN curl -o sphinx.tar.gz https://sphinxsearch.com/files/sphinx-2.2.11-release.tar.gz \&& tar -zxvf sphinx.tar.gz > /dev/null \&& curl -o libstemmer_c.tgz https://snowball.tartarus.org/dist/libstemmer_c.tgz \&& tar -xvzf libstemmer_c.tgz > /dev/null \&& cd sphinx-*/ \&& cp -R ../libstemmer_c/* ./libstemmer_c \&& ./configure --with-mysql --with-libstemmer > /dev/null \&& make > /dev/null && make install > /dev/null
During this step, you also need to clean up extraneous data to reduce the image size:
# CleanupRUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
Step 6: Mount Bundled Gems Folder Inside Container
Docker supports mount points called “volumes” which let you access data from either the native host or another container. In this case, you can mount your bundled gems folder inside the container:
ENV APP /appENV BUNDLE_PATH /bundleVOLUME /bundle# GemsRUN gem install bundlerADD Gemfile .ADD Gemfile.lock .ADD vendor .RUN bundle install --jobs 4 --retry 5
(To learn more about
BUNDLE_PATH
and why gems must be installed as the last step, see:
https://medium.com/@fbzga/how-to-cache-bundle-install-with-docker-7bed453a5800
)
Step 7: Install Node Packages
When using Docker to perform this step, the NPM is invoked with root privileges, so it will change the UID to the user account or to the UID specified by the user config, which, in this case, defaults to nobody. Start by setting the unsafe-perm flag to run scripts with root privileges:
ADD package.json .RUN npm install --unsafe-perm
(For more details on running scripts with root privileges, see:
https://docs.npmjs.com/misc/scripts#user
) During the last part of this step, you add the app’s code, expose the internal 3000 port, and process any incoming commands thought
bin/docker-entrypoint.sh
:
# AppRUN mkdir $APPWORKDIR $APPADD . $APPEXPOSE 3000ENTRYPOINT bin/docker-entrypoint.sh $0 $@
Step 8: Create docker-compose.yml File
Before executing any code in the app’s container, you need to make sure that the old PID files have been deleted and the database is available:
#!/bin/bashbundle check || bundle install --jobs 4 --retry 5pids=( server.pid realtime_updater.pid )for file in "${pids[@]}"dopath="tmp/pids/$file"if [ -f $path ]; thenrm $pathfidone./bin/wait-for-mysql.shecho "[INFO] Running in app: $@"exec "$@"
Now you’ll use
wait-for-mysql.sh
to hold the execution until the DB is ready:
#!/bin/bashecho "[INFO] Waiting for mysql"until mysql -h"$MYSQL_HOST" -P3306 -u"$MYSQL_ROOT_USER" -p"$MYSQL_ROOT_PASSWORD" -e 'show databases;'; do>&2 printf "."sleep 1doneecho "[INFO] Mysql ready"
Next, you need to integrate the app with Mysql, Mongo, Redis, volumes and expose the env variables. As an additional step, you can create
docker-compose-base.yml
which will be extended in the compose file for dev and ci/prod. Many of the images on Dockerhub accept env variables for configuration. In this case, you want to config mysql’s root user and expose the data in the application container, as well as in the Redis and Mongo URLs:
# .docker/.envREDIS_HOST=redisMONGO_HOST=mongo# .docker/db.envMYSQL_ROOT_USER=rootMYSQL_ROOT_PASSWORD=passwordMYSQL_USER=rootMYSQL_PASSWORD=passwordMYSQL_HOST=dbMYSQL_DATABASE=app_development
The
docker-compose.yml
includes the application service, along with the images for Mysql, Redis and Mongo:
# docker-compose.ymlversion: '2'services:app:build: .command: ./bin/startup.shvolumes:- .:/app- bundle:/bundleports:- '3000:3000'- '4000:4000'env_file:- ./.docker/db.env- ./.docker/.envlinks:- db- redis- mongodb:image: mysql:5.7ports:- 3306env_file:- ./.docker/db.envvolumes:- mysql:/var/lib/mysqlredis:image: redis:latestvolumes:- redis:/datamongo:image: mongovolumes:- mongo:/data/dbvolumes:bundle:mysql:redis:mongo:# Procfile.dockerweb: bundle exec rails s -b 0.0.0.0 -p 3000 -e ${RACK_ENV:-development}react: npm startresque: QUEUE=... bundle exec rake resque:worksphinx: bundle exec rake ts:start NODETACH=true....
#startup.sh#!/bin/bash# tells the bash script to exit whenever anything returns a non-zero return value.set -e./bin/wait-for-mysql.sh# echo "[INFO] Precreate databases..."# rake db:version || bundle exec rake db:setup# echo "[INFO] Running db:migrate..."# rake db:migratebundle exec foreman start -f Procfile.docker
At this point, you have completed all the steps needed to create your new Docker container and can move on to starting and running the application.
Step 9: Start and Run the Application
Your entry point:
docker-compose up
Point your browser to: https://localhost:3000 or https://192.168.99.100:3000. The Docker default machine’s IP is 192.168.99.100. To make sure your IP is correct, run:
docker-machine ip docker
If you want to bash to the app container or run custom command, don’t forget to enable and map services to the host by passing the “service-ports” option to
RUN command
:
docker-compose run --service-ports app bash
Here’s what you need to run the specs:
docker-compose run app bundle exec rspec
Step 10: Prepare to Release the App to Production
- Move applications logs to STDOUT/STDERR. During the updating process, logs will be saved to the container’s default file system.
- Integrate this STDOUT/STDERR log file into a centralized logging system. This is usually a 3rd party service.
- Move Sphinx, Node.js, Resque, wkhtmltopdf and all schedule-based tasks and processes to their own containers.
- Create a compose file for CI and prod.
Containerizing outdated apps with Docker is becoming increasingly popular due to its many benefits. It speeds up the entire development process while improving portability, transparency, and security.
More to read

Not all AI software development companies are equal. Learn what separates firms that truly build with AI from those that just use the word. Includes real questions to ask and red flags to avoid.

SaaS made sense a decade ago. For many businesses today, custom AI-powered software delivers better ROI, faster. Here’s how to know when to make the switch, and how to do it without disrupting your operations.

Dive into Sphere's full-stack developer journey with AI – from tackling code with GitHub Copilot to unleashing problem-solving insights with ChatGPT. Explore the potential of AI in software development projects: which tools are truly handy, how many hours can you save, and what's the next big thing? Pavel Korchak shares his insights.

In 2023, interoperability is still top of the agenda for leaders and decision-makers, referring to the timely and secure access, integration and use of electronic health data to optimize health outcomes for individuals and populations.