Skip to content

Commit

Permalink
Add support for wkhtmltopdf.
Browse files Browse the repository at this point in the history
bovender committed Mar 6, 2020
1 parent 1bf076f commit 94ec38a
Showing 6 changed files with 149 additions and 108 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# dora changelog

## Version 1.1.0 (2020-03-06)

### New feature

- Install [wkhtmltopdf](https://wkhtmltopdf.org/index.html) unless the
environment variable `$NO_WKHTMLTOPDF` is defined.

## Version 1.0.0 (2020-03-06)

Initial release.
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -17,6 +17,9 @@ ENV RAILS_DB_HOST ""
ENV RAILS_DB_NAME ${APP_NAME}
ENV RAILS_DB_USER ${APP_NAME}
ENV RAILS_DB_PASS ""
ENV WKHTMLTOPDF ""
ENV WKHTMLTOPDF_URL "https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox_0.12.5-1.bionic_amd64.deb"
ENV WKHTMLTOPDF_SUM "db48fa1a043309c4bfe8c8e0e38dc06c183f821599dd88d4e3cea47c5a5d4cd3"

# Install nodejs in passenger-docker's way
RUN mkdir /pd_build
@@ -52,6 +55,8 @@ RUN chmod +x /usr/local/bin/dora-banner.sh
RUN mkdir -p /etc/my_init.d
ADD bootstrap-container.sh /etc/my_init.d/10_bootstrap_container.sh
RUN chmod +x /etc/my_init.d/10_bootstrap_container.sh
ADD install-wkhtmltopdf.sh /etc/my_init.d/90_install_wkhtmltopdf.sh
RUN chmod +x /etc/my_init.d/90_install_wkhtmltopdf.sh

RUN mkdir -p /etc/service/sidekiq
ADD run-sidekiq.sh /etc/service/sidekiq/run
@@ -69,4 +74,4 @@ RUN cat /tmp/key.pub >> /home/app/.ssh/authorized_keys &&\
chmod 0600 /home/app/.ssh/authorized_keys

# Clean up APT when done.
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
191 changes: 93 additions & 98 deletions README.md
Original file line number Diff line number Diff line change
@@ -2,22 +2,20 @@

## *DO*cker container for *RA*ils

This is a little project that helps me to set up and operate
[Docker][] containers for [Ruby on Rails][] apps. It builds
upon the [passenger-docker][] container by [Phusion][], the
makers of the [Passenger][] app server.
This is a little project that helps me to set up and operate [Docker][]
containers for [Ruby on Rails][] apps. It builds upon the [passenger-docker][]
container by [Phusion][], the makers of the [Passenger][] app server.

The Dockerfile and the maintenance scripts are generic.
Customization for specific apps happens through Docker build
`ARGS` and environment variables.
The Dockerfile and the maintenance scripts are generic. Customization for
specific apps happens through Docker build `ARGS` and environment variables.
There is built-in support to generate PDF files with [wkhtmltopdf][].

> If you stumble upon this, be advised that this is amateur
work. It may suit your needs, but it was mainly created to
help me with my own projects. I would be more than happy though
to take pull request to improve this.
> If you stumble upon this, be advised that this is amateur work. It may suit
your needs, but it was mainly created to help me with my own projects. I would
be more than happy though to take pull request to improve this.

An alternative and much more sophisticated approach to
Dockerizing a Rails app can be found at [Discourse][].
An alternative and much more sophisticated approach to Dockerizing a Rails app
can be found at [Discourse][].

## Customization

@@ -44,6 +42,8 @@ Customization is mostly done with environment variables.
| `RAILS_SMTP_USER` | SMTP user name | `$APP_NAME`
| `RAILS_SMTP_PASS` | SMTP password |
| `SECRET_KEY_BASE` | Rails' secret key base |
| `NO_WKHTMLTOPDF` | Do not attempt to install [wkhtmltopdf][] | (empty)
| `WKHTMLTOPDF_URL` | Download URL for [wkhtmltopdf][] |

### Build argument

@@ -65,14 +65,12 @@ See below for more information about SSH'ing into the container.

### YAML snippet for docker-compose

To use this with [docker-compose][], clone the repository,
then add the following snippet to your `docker-compose.yml`
file and customize it (e.g., replace `MY_APP` with something
else).
To use this with [docker-compose][], clone the repository, then add the
following snippet to your `docker-compose.yml` file and customize it (e.g.,
replace `MY_APP` with something else).

The bracketed bits (`{{ ... }}`) are [Ansible][] variables.
If you do not use Ansible, just replace them with something
else.
The bracketed bits (`{{ ... }}`) are [Ansible][] variables. If you do not use
Ansible, just replace them with something else.

```yaml
MY_APP:
@@ -114,9 +112,8 @@ else.
- "POSTGRES_PASSWORD={{ postgres_master_password }}"
```
Snippet for Ansible's `defaults/main.yml` file (I define all variables
here, even those with a default value, to prevent surprises in the
future):
Snippet for Ansible's `defaults/main.yml` file (I define all variables here,
even those with a default value, to prevent surprises in the future):

```yaml
docker:
@@ -145,13 +142,11 @@ MY_APP:
pass:
```

Remember to use `ansible-vault encrypt_string` to hash all
passwords!
Remember to use `ansible-vault encrypt_string` to hash all passwords!

> **WARNING:** Even then using `ansible-vault` to encrypt all
secrets in your Ansible repository, be aware that they will
appear unencrypted in the `docker-compose.yml` file that is
deployed on the server!
> **WARNING:** Even then using `ansible-vault` to encrypt all secrets in your
Ansible repository, be aware that they will appear unencrypted in the
`docker-compose.yml` file that is deployed on the server!

Please ensure your secrets are safe.

@@ -199,10 +194,9 @@ end

### Reverse proxy

I use [Apache2][] as a reverse proxy to relay requests from
the Docker host to the container. This can of course also be
done with [Nginx][] or any other web server that can act as
a reverse proxy, but I have more experience with Apache.
I use [Apache2][] as a reverse proxy to relay requests from the Docker host to
the container. This can of course also be done with [Nginx][] or any other web
server that can act as a reverse proxy, but I have more experience with Apache.

NB: This is an [Ansible][] template with some Ansible variables
in it.
@@ -247,30 +241,28 @@ in it.

## Sidekiq

The Dockerfile installs a service into `/etc/services/sidekiq`
that runs [Sidekiq][] in the app directory. The Sidekiq log is
written to `/shared/log/sidekiq.log`.
The Dockerfile installs a service into `/etc/services/sidekiq` that runs
[Sidekiq][] in the app directory. The Sidekiq log is written to
`/shared/log/sidekiq.log`.

There is currently no sanity check, so make sure your `Gemfile`
bundles Sidekiq.
There is currently no sanity check, so make sure your `Gemfile` bundles
Sidekiq.

## Upgrading the app

To upgrade the app, call the `upgrade-app.sh` script that
the `Dockerfile` places in `/usr/local/bin`. The script will
pull the app from the [Git][] repository, migrate the database,
precompile assets, and restart [Passenger][].
To upgrade the app, call the `upgrade-app.sh` script that the `Dockerfile`
places in `/usr/local/bin`. The script will pull the app from the [Git][]
repository, migrate the database, precompile assets, and restart [Passenger][].

There is no good contingency plan for when any of these steps
fail. The `upgrade-app.sh` script provides only very limited
support to roll back the application to a previous state.
One tool that is definitively better at this is [Capistrano][].
There is no good contingency plan for when any of these steps fail. The
`upgrade-app.sh` script provides only very limited support to roll back the
application to a previous state. One tool that is definitively better at this
is [Capistrano][].

## Data persistence

Data can be persisted with a Docker volume that is mounted
onto `/shared`. The maintenance scripts link several directories
into `/shared`:
Data can be persisted with a Docker volume that is mounted onto `/shared`. The
maintenance scripts link several directories into `/shared`:

- `/home/app/app/vendor/bundle` (which contains the bundled Gems)
- `/home/app/app/log` (Rails' log files)
@@ -310,74 +302,76 @@ Then you can simply log into your Rails container from your workstation:
ssh my_rails_app
```

## wkhtmltopdf support

To facilitate generating PDF files, Dora has built-in support to install
[wkhtmltopdf][]. When the container is started, Dora checks for the presence
of the `wkhtmltopdf` command. If it is not found, the binary will be downloaded
from Github and installed along with the required dependencies.

Define the `$NO_WKKHTMLTOPDF` environment variable with any value to prevent
Dora from installing [wkhtmltopdf][].

You can customize the download by overriding `$WKHTMLTOPDF_URL`. Just do not
forget to also place the SHA-256 checksum into `$WKHTMLTOPDF_SUM`.

## Development and testing

To use `dora` for development and testing, you may want to
set `$GIT_PULL` to `false` and mount your entire Rails application's
directory onto `/home/app`.
To use `dora` for development and testing, you may want to set `$GIT_PULL` to
`false` and mount your entire Rails application's directory onto `/home/app`.

With `$GIT_PULL` set to `false`, it is assumed that the
entire `/home/app/app` directory is a mounted Docker volume.
The bootstrapping script will _not_ link directories to
`/shared/...`. It _will_ however set Bundler's `path` config
option to `vendor/bundle` (even though it does not set
`deployment` mode), so that Gems are saved in the mounted
volume. This speeds up rebuilding the container.
With `$GIT_PULL` set to `false`, it is assumed that the entire `/home/app/app`
directory is a mounted Docker volume. The bootstrapping script will _not_ link
directories to `/shared/...`. It _will_ however set Bundler's `path` config
option to `vendor/bundle` (even though it does not set `deployment` mode), so
that Gems are saved in the mounted volume. This speeds up rebuilding the
container.

`dora` ships with a generic `docker-compose.yml` file that
can be customized via environment variables. A `.env` file
lends itself well to this configuration. The composition
consists of the rails app, Postgres, and Redis. See `sample.env`
for usage instructions.
`dora` ships with a generic `docker-compose.yml` file that can be customized
via environment variables. A `.env` file lends itself well to this
configuration. The composition consists of the rails app, Postgres, and Redis.
See `sample.env` for usage instructions.

## Troubleshooting

### Sending mail

If your mail server is secured by a firewall, make sure
it accepts connections from the Docker network.
If your mail server is secured by a firewall, make sure it accepts connections
from the Docker network.

### Avoiding confusion

One thing that I initially had quite a hard time wrapping my
head around is the distinction between an image and a container.
However, this distinction is quite important in practice:

When the container is being built, any and all external
dependencies such as mounted volumes and of course the
database server are _not available_. This seems trivial, but
I struggled with it initially.

The _container_ on the other hand has all these dependencies
available, but it may need some initial bootstrapping when
it is first started. [Discourse][] takes care of this with
an external control script called `launcher`. I prefer to
have my container as atomic as possible. Therefore I
decided to place the bootstrapping commands in a script that
is run whenever the container is started, but checks for the
presence of a sentinel file to decide whether bootstrapping
is needed or not. This avoids unnecessary and possibly time
consuming tasks such as precompiling assets, migrating the
database and so on.

A note on the **user name** and **application directory**:
`passenger-docker` creates a user called `app`. `dora`
installs the application into a directory in this user's
home directory that is also called `app`. Thus, the directory
One thing that I initially had quite a hard time wrapping my head around is the
distinction between an image and a container. However, this distinction is
quite important in practice:

When the container is being built, any and all external dependencies such as
mounted volumes and of course the database server are _not available_. This
seems trivial, but I struggled with it initially.

The _container_ on the other hand has all these dependencies available, but it
may need some initial bootstrapping when it is first started. [Discourse][]
takes care of this with an external control script called `launcher`. I prefer
to have my container as atomic as possible. Therefore I decided to place the
bootstrapping commands in a script that is run whenever the container is
started, but checks for the presence of a sentinel file to decide whether
bootstrapping is needed or not. This avoids unnecessary and possibly time
consuming tasks such as precompiling assets, migrating the database and so on.

A note on the **user name** and **application directory**: `passenger-docker`
creates a user called `app`. `dora` installs the application into a directory
in this user's home directory that is also called `app`. Thus, the directory
where the Rails application is installed is:

```bash
/home/app/app
```

Initially I had intended to make the application directory
configurable to avoid possible confusion with the `app`
user, but it would have been overly complicated to
adjust the Nginx server configuration to this custom
directory, at least if an environment variable was involved.
Therefore, the `app` directory is now hard-coded into
`dora`.
Initially I had intended to make the application directory configurable to
avoid possible confusion with the `app` user, but it would have been overly
complicated to adjust the Nginx server configuration to this custom directory,
at least if an environment variable was involved. Therefore, the `app`
directory is now hard-coded into `dora`.

## Further reading
- [Discourse's Docker container][Discourse]
@@ -401,3 +395,4 @@ MIT license. See [`LICENSE`](LICENSE).
[Phusion]: https://www.phusion.nl
[Ruby on Rails]: https://rubyonrails.org
[Sidekiq]: https://github.com/mperham/sidekiq
[wkhtmltopdf]: https://wkhtmltopdf.org
17 changes: 8 additions & 9 deletions bootstrap-container.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
#!/usr/bin/env bash

# This script serves to boostrap the container.
# This script is run every time the container is started,
# but we do not want to re-compile the assets over and
# over again and so on, so we use a flag file to determine
# whether the container has already been bootstrapped or not.
# Bootstrapping could be done by an external script (see
# Discourse's launcher script for instance), but we prefer
# to have all the tools that we need in the container
# itself, without a need for an external control script.
# This script is run every time the container is started, but we do not want to
# re-compile the assets over and over again and so on, so we use a flag file to
# determine whether the container has already been bootstrapped or not.
# Bootstrapping could be done by an external script (see Discourse's launcher
# script for instance), but we prefer to have all the tools that we need in the
# container itself, without a need for an external control script.

dora-banner.sh | tee | grep -iv pass > /etc/ssh/dora-banner
dora-banner.sh
dora-banner.sh | grep -v _PASS > /etc/ssh/dora-banner

FLAG_FILE=/bootstrapped
if [ -a $FLAG_FILE ]; then
2 changes: 2 additions & 0 deletions dora-banner.sh
Original file line number Diff line number Diff line change
@@ -29,6 +29,8 @@ echo "= RAILS_DB_PASS: $RAILS_DB_PASS"
echo "= RAILS_SMTP_HOST: $RAILS_SMTP_HOST"
echo "= RAILS_SMTP_USER: $RAILS_SMTP_USER"
echo "= RAILS_SMTP_PASS: $RAILS_SMTP_PASS"
echo "= NO_WKHTMLTOPDF: $NO_WKHTMLTOPDF"
echo "= \`which wkhtmltopdf\`: $(which wkhtmltopdf)"
echo
echo "======================================="
echo "=== https://github.com/bovender/dora =="
28 changes: 28 additions & 0 deletions install-wkhtmltopdf.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash

DEST_FILE=/tmp/wkhtmltopdf.deb

if [ $(id -u) -ne 0 ]; then
echo "This script must be run as root. It serves to install wkhtmltopdf."
exit 1
fi

if which wkhtmltopdf; then
echo "wkhtmltopdf already installed: at $(which wkhtmltpdf)"
wkhtmltopdf -V
exit 0
fi

if [ "$NO_WKHTMLTOPDF" != "" ]; then
echo "\`\$NO_WKHTMLTOPDF\` is set to $NO_WKHTMLTOPDF"
echo "Will not install wkhtmltopdf."
exit 0
fi

echo "wkhtmltopdf not found, attempting to download and install"
set -x -e
curl -L -s "$WKHTMLTOPDF_URL" -o $DEST_FILE
echo "$WKHTMLTOPDF_SUM $DEST_FILE" | sha256sum -c

apt-get update
dpkg -i $DEST_FILE || apt-get install -f -y --no-install-recommends

0 comments on commit 94ec38a

Please sign in to comment.