Calibre Web

Dockerizing Calibre-web

Those like me who use Calibre to manage their ereader library are familiar with the hassle of syncing between the two. Standard choices are to either 1) physically hook up your ereader to the computer where Calibre is installed, or 2) access Calibre server or a similar service over your local network. I am not a fan of the former, and haven’t found an easy sync method with the latter. Recently I came across the web app Calibre-web, which offers a great online alternative along with a polished UI for online ebook management.

Calibre-web is python-based, and like most python applications, should be installed in an isolated environment to avoid issues with pre-existing dependencies. This can be achieved with either a virtual environment or a docker container. I have used both, and on balance find docker to be easier to deploy. This is especially true because a pre-built docker image is already available, and a good how-to on running it on your machine.

Here I’ll describe how to build your own docker image for Calibre-web. The main reason for this exercise is that it is possible to greatly reduce the image size by using a multi-stage Dockerfile. Besides, working with docker is always a good practice. To follow along, you need Docker installed on your machine. I prefer working with terminals, and use docker CLI (command line interface) on a linux platform. You may instead prefer installing docker desktop that offers a simpler interface.

In the first step, I created a working directory ~/Documents/CalibreWeb, moved into it, and installed the two packages calibreweb and jsonschema (jsonschema is optional and needed for syncing with Kobo ereader) using pipenv in a python virtual environment to generate the requirements.txt file, with the following commands on a terminal:

> mkdir ~/Documents/CalibreWeb
> cd ~/Documents/CalibreWeb
> pipenv install calibreweb jsonschema
> pipenv run pip freeze > requirements.txt

A requirements.txt file is strictly not necessary; instead one can directly install the two packages with Dockerfile. The benefit of requirements.txt is that the versions of these packages and all dependencies are frozen, which makes the docker image stable against future package upgrades, and also portable so everyone will install the same versions.

Single-stage dockerfile

Next step is creating the Dockerfile. First a simple Dockerfile that has just one stage: it starts with one base image and creates one final image.

# get latest stable python image
FROM python:3.10.7-bullseye

# set working directory (inside container)
WORKDIR /app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# create virtual environment (inside container)
RUN python -m venv .venv
ENV PATH="/app/.venv/bin:$PATH"

# install packages (including dependencies)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# run command
CMD ["cps"]

The code is simple enough: it pulls the latest stable python image available as of now, creates a working directory /app and a python virtual environment in the container, appends the bin folder of venv to the start of the path (which is what activating venv does), copies requirements.txt from host to container, installs packages and dependencies, and runs the executable cps (from /app/.venv/bin). I also assigned the ENV variable PYTHONDONTWRITEBYTECODE to prevent python from writing out pyc files, and PYTHONUNBUFFERED to prevent buffering stdin/stdout.

Now let’s build the image and check its size:

> docker build -t calibreweb-run .
>
> docker images
  REPOSITORY                TAG               IMAGE ID       CREATED         SIZE
  calibreweb-run            latest            d3b4e56b43c1   7 seconds ago   1.06GB
  python                    3.10.7-bullseye   da84e66c3a7c   13 days ago     921MB

The image calibreweb-run is 1.06GB, hardly a surprise given that the base image itself is 921MB.

Multi-stage dockerfile

This is where a multi-stage Dockerfile becomes a better alternative. Let’s see this with a two-stage Dockerfile: a build stage using the same python base image as before, followed by a run stage using a much smaller python image.

# build stage
FROM python:3.10.7-bullseye AS build
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN python -m venv .venv
ENV PATH="/app/.venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# run stage
FROM python:3.10.7-alpine3.16
WORKDIR /app
COPY --from=build /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
CMD ["cps"]

The build stage is nearly identical to the earlier Dockerfile (except the CMD instruction is now in the run stage). This is where all the python packages and dependencies are installed. The run stage pulls the latest stable python:alpine image as its base image (typically smallest of the available python images), creates the same working directory /app in this new container, copies the python environment including all installed packages from the build stage, appends to path as before, and executes cps.

Let’s build the image by running docker build twice, and check image sizes with docker images:

> docker build -t calibreweb-build --target build .
> docker build -t calibreweb-run .
>
> docker images
  REPOSITORY                TAG                 IMAGE ID       CREATED          SIZE
  calibreweb-run            latest              4de0c7777063   4 seconds ago    181MB
  calibreweb-build          latest              9d71247e3e28   40 seconds ago   1.06GB
  python                    3.10.7-alpine3.16   2bbcf2f78d64   11 days ago      48.7MB
  python                    3.10.7-bullseye     da84e66c3a7c   13 days ago      921MB

First command targets the build stage and generates the image tagged calibreweb-build, and the second command creates the run-stage image calibreweb-run. First command is not necessary; the second command alone still creates the same two images, except that the build-stage image gets a default <none> tag. I prefer explicit tagging of all intermediate stages.

Now there is a tiny python:alpine image, together with the huge build-stage image as expected, and the new much leaner run-stage image calibreweb-run.

To create the container, I run the image calibreweb-run in detached mode (to get the shell back) with this script :

#!/usr/bin/env bash
docker run -d \
    --name=calibreweb \
    -p 8083:8083 \
    -v $HOME/.calibre-web:/config \
    -v $HOME/Documents/Calibre\ Library:/books \
    calibreweb-run

The container port 8083 is mapped to the host port 8083 for web access (to the Calibre library) via http://<your IP address>:8083, and the two host folders, $HOME/.calibre-web for configuration settings and $HOME/Documents/Calibre Library (where I store my library), are mounted to the container folders /config and /books via volume mapping. Once the container is up and running, Kobo sync can be set up by following the instructions given here and here.

Gravatar

Hi there! I am Roy, founder of Quantiux and author of everything you see here. I am a data scientist, with a deep interest in playing with data of all colors, shapes and sizes. I also enjoy coding anything that catches my fancy.

Leave a comment below if this article interests you, or contact me for any general comment/question.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top