Skip to content

StepsAway/passenger-docker

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Docker base images for Ruby web apps

Phusion Passenger

Docker

Passenger-docker is a set of Docker images meant to serve as good bases for Ruby web app images. In line with Phusion Passenger's goal, passenger-docker's goal is to make Docker image building for web apps much easier and faster.

Why is this image called "passenger"? It's to represent the ease: you just have to sit back and watch most of the heavy lifting being done for you. Passenger-docker is part of a larger and more ambitious project: to make web app deployment ridiculously simple, to heights never achieved before.

Relevant links: Github | Docker registry | Blog


Table of contents


Why use passenger-docker?

Why use passenger-docker instead of doing everything yourself in Dockerfile?

  • Your Dockerfile can be smaller.
  • It reduces the time needed to write a correct Dockerfile. You won't have to worry about the base system and the stack, you can focus on just your app.
  • It drastically reduces the time needed to run docker build, allowing you to iterate your Dockerfile more quickly.
  • It reduces download time during redeploys. Docker only needs to download the base image once: during the first deploy. On every subsequent deploys, only the changes you make on top of the base image are downloaded.

What's included?

Passenger-docker is built on top of a solid base: baseimage-docker.

Basics (learn more at baseimage-docker):

  • Ubuntu 14.04 LTS as base system.
  • A correct init process (learn more).
  • Fixes APT incompatibilities with Docker.
  • syslog-ng.
  • The cron daemon.
  • Runit for service supervision and management.

Language support:

  • Ruby 2.2 and 2.3;
    • MRI Ruby is installed via source
  • A build system, git, and development headers for many popular libraries, so that the most popular Ruby and Node.js native extensions can be compiled without problems.

Web server and application server:

  • Nginx 1.6. Enabled by default.
  • Phusion Passenger 5. Enabled by default (because it starts along with Nginx).
    • This is a fast and lightweight tool for simplifying web application integration into Nginx.
    • It adds many production-grade features, such as process monitoring, administration and status inspection.
    • It replaces (G)Unicorn, Thin, Puma, uWSGI.

Memory efficiency

Passenger-docker is very lightweight on memory. In its default configuration, it only uses 10 MB of memory (the memory consumed by bash, runit, syslog-ng, etc).

Image variants

Passenger-docker consists of several images, each one tailor made for a specific user group.

Ruby images

  • stepsaway/passenger-ruby22 - Ruby 2.1.*
  • stepsaway/passenger-ruby23 - Ruby 2.2.*

Inspecting the image

To look around in the image, run:

docker run --rm -t -i stepsaway/passenger-ruby23 bash -l

You don't have to download anything manually. The above command will automatically pull the passenger-docker image from the Docker registry.

Using the image as base

Getting started

There are several images, e.g. stepsaway/passenger-ruby22 and stepsaway/passenger-ruby23. Choose the one you want. See Image variants.

So put the following in your Dockerfile:

# To make your builds reproducible, make
# sure you lock down to a specific version, not to `latest`!
# See https://github.com/stepsaway/passenger-docker/blob/master/Changelog.md for
# a list of version numbers.
#FROM stepsaway/passenger-ruby22:<VERSION>
#FROM stepsaway/passenger-ruby23:<VERSION>

# Set correct environment variables.
ENV HOME /root

# Use baseimage-docker's init process.
CMD ["/sbin/my_init"]

# ...put your own build instructions here...

# Clean up APT when done.
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

The app user

The image has an app user with UID 9999 and home directory /home/app. Your application is supposed to run as this user. Even though Docker itself provides some isolation from the host OS, running applications without root privileges is good security practice.

Your application should be placed inside /home/app.

Using Nginx and Passenger

Before using Passenger, you should familiarise yourself with it by reading its documentation.

Nginx and Passenger are enabled by default. Disabled them like so:

RUN touch /etc/service/nginx/down

Adding your web app to the image

Passenger works like a mod_ruby, mod_nodejs, etc. It changes Nginx into an application server and runs your app from Nginx. So to get your web app up and running, you just have to add a virtual host entry to Nginx which describes where you app is, and Passenger will take care of the rest.

You can add a virtual host entry (server block) by placing a .conf file in the directory /etc/nginx/sites-enabled. For example:

# /etc/nginx/sites-enabled/webapp.conf:
server {
    listen 80;
    server_name www.webapp.com;
    root /home/app/webapp/public;

    # The following deploys your Ruby app on Passenger.

    passenger_enabled on;
    passenger_user app;

    # Specify Ruby location:
    passenger_ruby /usr/bin/ruby;

}

# Dockerfile:
RUN rm /etc/nginx/sites-enabled/default
ADD webapp.conf /etc/nginx/sites-enabled/webapp.conf
RUN mkdir /home/app/webapp
RUN ...commands to place your web app in /home/app/webapp...

Configuring Nginx

The best way to configure Nginx is by adding .conf files to /etc/nginx/main.d and /etc/nginx/conf.d. Files in main.d are included into the Nginx configuration's main context, while files in conf.d are included in the Nginx configuration's http context.

For example:

# /etc/nginx/main.d/secret_key.conf:
env SECRET_KEY=123456;

# /etc/nginx/conf.d/gzip_max.conf:
gzip_comp_level 9;

# Dockerfile:
ADD secret_key.conf /etc/nginx/main.d/secret_key.conf
ADD gzip_max.conf /etc/nginx/conf.d/gzip_max.conf

Setting environment variables in Nginx

By default Nginx clears all environment variables (except TZ) for its child processes (Passenger being one of them). That's why any environment variables you set with docker run -e, Docker linking and /etc/container_environment, won't reach Nginx.

To preserve these variables, place an Nginx config file ending with *.conf in the directory /etc/nginx/main.d, in which you tell Nginx to preserve these variables. For example when linking a PostgreSQL container or MongoDB container:

# /etc/nginx/main.d/postgres-env.conf:
env POSTGRES_PORT_5432_TCP_ADDR;
env POSTGRES_PORT_5432_TCP_PORT;

# Dockerfile:
ADD postgres-env.conf /etc/nginx/main.d/postgres-env.conf

By default, passenger-docker already contains a config file /etc/nginx/main.d/default.conf which preserves the PATH environment variable.

Application environment name (RAILS_ENV, NODE_ENV, etc)

Some web frameworks adjust their behavior according to the value some environment variables. For example, Rails respects RAILS_ENV while Connect.js respects NODE_ENV. By default, Phusion Passenger sets all of the following environment variables to the value production:

  • RAILS_ENV
  • RACK_ENV
  • WSGI_ENV
  • NODE_ENV
  • PASSENGER_APP_ENV

Setting these environment variables yourself (e.g. using docker run -e RAILS_ENV=...) will not have any effect, because Phusion Passenger overrides all of these environment variables. The only exception is PASSENGER_APP_ENV (see below).

With passenger-docker, there are two ways to set the aforementioned environment variables. The first is through the passenger_app_env config option in Nginx. For example:

# /etc/nginx/sites-enabled/webapp.conf:
server {
    ...
    # Ensures that RAILS_ENV, NODE_ENV, etc are set to "staging"
    # when your application is started.
    passenger_app_env staging;
}

The second way is by setting the PASSENGER_APP_ENV environment variable from docker run

docker run -e PASSENGER_APP_ENV=staging YOUR_IMAGE

This works because passenger-docker autogenerates an Nginx configuration file (/etc/nginx/conf.d/00_app_env.conf) during container boot. This file sets the passenger_app_env option in the http context. This means that if you already set passenger_app_env in the server context, running docker run -e PASSENGER_APP_ENV=... won't have any effect!

If you want to set a default value while still allowing that to be overridden by docker run -e PASSENGER_APP_ENV=, then instead of specifying passenger_app_env in your Nginx config file, you should create a /etc/nginx/conf.d/00_app_env.conf. This file will be overwritten if the user runs docker run -e PASSENGER_APP_ENV=....

# /etc/nginx/conf.d/00_app_env.conf
# File will be overwritten if user runs the container with `-e PASSENGER_APP_ENV=...`!
passenger_app_env staging;

Additional daemons

You can add additional daemons to the image by creating runit entries. You only have to write a small shell script which runs your daemon, and runit will keep it up and running for you, restarting it when it crashes, etc.

The shell script must be called run, must be executable, and is to be placed in the directory /etc/service/<NAME>.

Here's an example showing you how to a memached server runit entry can be made.

### In memcached.sh (make sure this file is chmod +x):
#!/bin/sh
# `setuser` is part of baseimage-docker. `setuser mecached xxx...` runs the given command
# (`xxx...`) as the user `memcache`. If you omit this, the command will be run as root.
exec /sbin/setuser memcache /usr/bin/memcached >>/var/log/memcached.log 2>&1

### In Dockerfile:
RUN mkdir /etc/service/memcached
ADD memcached.sh /etc/service/memcached/run

Note that the shell script must run the daemon without letting it daemonize/fork it. Usually, daemons provide a command line flag or a config file option for that.

Tip: If you're thinking about running your web app, consider deploying it on Passenger instead of on runit. Passenger relieves you from even having to write a shell script, and adds all sorts of useful production features like process scaling, introspection, etc. These are not available when you're only using runit.

Running scripts during container startup

passenger-docker uses the baseimage-docker init system, /sbin/my_init. This init system runs the following scripts during startup, in the following order:

  • All executable scripts in /etc/my_init.d, if this directory exists. The scripts are run during in lexicographic order.
  • The script /etc/rc.local, if this file exists.

All scripts must exit correctly, e.g. with exit code 0. If any script exits with a non-zero exit code, the booting will fail.

The following example shows how you can add a startup script. This script simply logs the time of boot to the file /tmp/boottime.txt.

### In logtime.sh (make sure this file is chmod +x):
#!/bin/sh
date > /tmp/boottime.txt

### In Dockerfile:
RUN mkdir -p /etc/my_init.d
ADD logtime.sh /etc/my_init.d/logtime.sh

Upgrading the operating system inside the container

passenger-docker images contain an Ubuntu 14.04 operating system. You may want to update this OS from time to time, for example to pull in the latest security updates. OpenSSL is a notorious example. Vulnerabilities are discovered in OpenSSL on a regular basis, so you should keep OpenSSL up-to-date as much as you can.

While we release passenger-docker images with the latest OS updates from time to time, you do not have to rely on us. You can update the OS inside passenger-docker images yourself, and it is recommend that you do this instead of waiting for us.

To upgrade the OS in the image, run this in your Dockerfile:

RUN apt-get update && apt-get upgrade -y -o Dpkg::Options::="--force-confold"

Container administration

One of the ideas behind Docker is that containers should be stateless, easily restartable, and behave like a black box. However, you may occasionally encounter situations where you want to login to a container, or to run a command inside a container, for development, inspection and debugging purposes. This section describes how you can administer the container for those purposes.

Tip: passenger-docker is based on baseimage-docker. Please consult the baseimage-docker documentation for more container administration documentation and tips.

Running a one-shot command in a new container

Note: This section describes how to run a command insider a -new- container. To run a command inside an existing running container, see Running a command in an existing, running container.

Normally, when you want to create a new container in order to run a single command inside it, and immediately exit after the command exits, you invoke Docker like this:

docker run YOUR_IMAGE COMMAND ARGUMENTS...

However the downside of this approach is that the init system is not started. That is, while invoking COMMAND, important daemons such as cron and syslog are not running. Also, orphaned child processes are not properly reaped, because COMMAND is PID 1.

Passenger-docker provides a facility to run a single one-shot command, while solving all of the aforementioned problems. Run a single command in the following manner:

docker run YOUR_IMAGE /sbin/my_init -- COMMAND ARGUMENTS ...

This will perform the following:

  • Runs all system startup files, such as /etc/my_init.d/* and /etc/rc.local.
  • Starts all runit services.
  • Runs the specified command.
  • When the specified command exits, stops all runit services.

For example:

$ docker run stepsaway/passenger-ruby23:<VERSION> /sbin/my_init -- ls
*** Running /etc/rc.local...
*** Booting runit daemon...
*** Runit started as PID 80
*** Running ls...
bin  boot  dev  etc  home  image  lib  lib64  media  mnt  opt  proc  root  run  sbin  selinux  srv  sys  tmp  usr  var
*** ls exited with exit code 0.
*** Shutting down runit daemon (PID 80)...
*** Killing all processes...

You may find that the default invocation is too noisy. Or perhaps you don't want to run the startup files. You can customize all this by passing arguments to my_init. Invoke docker run YOUR_IMAGE /sbin/my_init --help for more information.

The following example runs ls without running the startup files and with less messages, while running all runit services:

$ docker run stepsaway/passenger-ruby23:<VERSION> /sbin/my_init --skip-startup-files --quiet -- ls
bin  boot  dev  etc  home  image  lib  lib64  media  mnt  opt  proc  root  run  sbin  selinux  srv  sys  tmp  usr  var

Running a command in an existing, running container

There are two ways to run a command inside an existing, running container.

Both way have their own pros and cons, which you can learn in their respective subsections.

Login to the container, or running a command inside it, via docker exec

You can use the docker exec tool on the Docker host OS to login to any container that is based on passenger-docker. You can also use it to run a command inside a running container. docker exec works by using Linux kernel system calls.

Here's how it compares to using SSH to login to the container or to run a command inside it:

  • Pros
    • Does not require running an SSH daemon inside the container.
    • Does not require setting up SSH keys.
    • Works on any container, even containers not based on passenger-docker.
  • Cons
    • If the docker exec process on the host is terminated by a signal (e.g. with the kill command or even with Ctrl-C), then the command that is executed by docker exec is not killed and cleaned up. You will either have to do that manually, or you have to run docker exec with -t -i.
    • Requires privileges on the Docker host to be able to access the Docker daemon. Note that anybody who can access the Docker daemon effectively has root access.
    • Not possible to allow users to login to the container without also letting them login to the Docker host.

Usage

Start a container:

docker run YOUR_IMAGE

Find out the ID of the container that you just ran:

docker ps

Now that you have the ID, you can use docker exec to run arbitrary commands in the container. For example, to run echo hello world:

docker exec YOUR-CONTAINER-ID echo hello world

To open a bash session inside the container, you must pass -t -i so that a terminal is available:

docker exec -t -i YOUR-CONTAINER-ID bash -l

Login to the container, or running a command inside it, via SSH

You can use SSH to login to any container that is based on passenger-docker. You can also use it to run a command inside a running container.

Here's how it compares to using docker exec to login to the container or to run a command inside it:

  • Pros
    • Does not require root privileges on the Docker host.
    • Allows you to let users login to the container, without letting them login to the Docker host. However, this is not enabled by default because passenger-docker does not expose the SSH server to the public Internet by default.
  • Cons
    • Requires setting up SSH keys. However, passenger-docker makes this easy for many cases through a pregenerated, insecure key. Read on to learn more.

Enabling SSH

Passenger-docker disables the SSH server by default. Add the following to your Dockerfile to enable it:

RUN rm -f /etc/service/sshd/down

# Regenerate SSH host keys. Passenger-docker does not contain any, so you
# have to do that yourself. You may also comment out this instruction; the
# init system will auto-generate one during boot.
RUN /etc/my_init.d/00_regen_ssh_host_keys.sh

About SSH keys

First, you must ensure that you have the right SSH keys installed inside the container. By default, no keys are installed, so nobody can login.

Using your own key

Edit your Dockerfile to install an SSH public key:

## Install an SSH of your choice.
ADD your_key.pub /tmp/your_key.pub
RUN cat /tmp/your_key.pub >> /root/.ssh/authorized_keys && rm -f /tmp/your_key.pub

Then rebuild your image. Once you have that, start a container based on that image:

docker run your-image-name

Find out the ID of the container that you just ran:

docker ps

Once you have the ID, look for its IP address with:

docker inspect -f "{{ .NetworkSettings.IPAddress }}" <ID>

Now that you have the IP address, you can use SSH to login to the container, or to execute a command inside it:

# Login to the container
ssh -i /path-to/your_key root@<IP address>

# Running a command inside the container
ssh -i /path-to/your_key root@<IP address> echo hello world

The docker-ssh tool

Looking up the IP of a container and running an SSH command quickly becomes tedious. Luckily, we provide the docker-ssh tool which automates this process. This tool is to be run on the Docker host, not inside a Docker container.

First, install the tool on the Docker host:

curl --fail -L -O https://github.com/phusion/baseimage-docker/archive/master.tar.gz && \
tar xzf master.tar.gz && \
sudo ./baseimage-docker-master/install-tools.sh

Then run the tool as follows to login to a container using SSH:

docker-ssh YOUR-CONTAINER-ID

You can lookup YOUR-CONTAINER-ID by running docker ps.

By default, docker-ssh will open a Bash session. You can also tell it to run a command, and then exit:

docker-ssh YOUR-CONTAINER-ID echo hello world

Inspecting the status of your web app

If you use Passenger to deploy your web app, run:

passenger-status
passenger-memory-stats

Logs

If anything goes wrong, consult the log files in /var/log. The following log files are especially important:

  • /var/log/nginx/error.log
  • /var/log/syslog
  • Your app's log file in /home/app.

Switching to Phusion Passenger Enterprise

If you are a Phusion Passenger Enterprise customer, then you can switch to the Enterprise variant as follows.

  1. Login to the Customer Area.

  2. Download the license key and store it in the same directory as your Dockerfile.

  3. Insert into your Dockerfile:

    ADD passenger-enterprise-license /etc/passenger-enterprise-license
    RUN echo deb https://download:[email protected]/enterprise_apt xenial main > /etc/apt/sources.list.d/passenger.list
    RUN apt-get update && apt-get install -y -o Dpkg::Options::="--force-confold" passenger-enterprise nginx-extras
    

    Replace $DOWNLOAD_TOKEN with your actual download token, as found in the Customer Area.

Building the image yourself

If for whatever reason you want to build the image yourself instead of downloading it from the Docker registry, follow these instructions.

Clone this repository:

git clone https://github.com/stepsaway/passenger-docker.git
cd passenger-docker

Build one of the images:

make build_ruby19
make build_ruby20
make build_ruby21
make build_ruby22
make build_jruby90
make build_nodejs
make build_customizable
make build_full

If you want to call the resulting image something else, pass the NAME variable, like this:

make build NAME=joe/passenger

About

Docker base images for Ruby web apps

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Shell 52.7%
  • Makefile 47.3%