A Proper Docker Image for WebCalendar - Projects Rocks
Skip to content
A Proper Docker Image for WebCalendar: SQLite-Backed,<br>Multi-Arch, and Self-Contained
Published 12th of July 2026
I recently needed a simple, self-hosted calendar solution and<br>chose<br>WebCalendar. It’s a mature PHP application that remains reliable and<br>functional. However, the upstream Docker implementation had a<br>design choice that conflicted with my containerization<br>principles: it relies on bind-mounting the application source<br>code from the host into the container.
I prefer containers to be immutable and self-contained. The host<br>should only manage state —data that must persist across<br>rebuilds—not the application binaries or source code. To address<br>this, I created<br>webcalendar-docker, a reimagined packaging of WebCalendar that keeps everything<br>inside the image except for what strictly needs to survive.
Key Design Decisions
Building from Git, not Release Zips.<br>This decision was driven by a packaging inconsistency in the<br>upstream<br>v1.9.19 release zip. The archive references<br>includes/mcp-loader.php, but the file is missing.<br>The git repository has contained this file since<br>v1.9.14. By building from a pinned git reference<br>(WEBCALENDAR_REF, defaulting to<br>v1.9.19), we avoid patching workarounds and ensure<br>a complete source tree.
SQLite over MySQL/MariaDB.<br>Upstream Compose files typically wire up MariaDB or PostgreSQL.<br>For single-user or small-team deployments, this introduces<br>unnecessary complexity: an extra container, connection<br>management, and startup ordering dependencies. SQLite handles<br>this workload efficiently within a single file, reducing the<br>infrastructure footprint to one container.
Strict Separation of Code and State.<br>Only two directories are bind-mounted to the host:<br>data/ (for the SQLite database) and<br>includes/ (for generated configuration files like<br>settings.php). Everything else—the PHP runtime, web<br>server configuration, and WebCalendar source—is baked into the<br>image. You can rebuild the image or destroy the container<br>without risking your data or configuration.
Multi-Architecture Support.<br>GitHub Actions automatically builds and publishes images for<br>both<br>amd64 and arm64. This ensures seamless<br>deployment on Raspberry Pi devices or other ARM-based servers<br>without requiring manual local builds.
Documented Upstream Quirks.<br>Enabling "single-user mode" in the install wizard currently<br>triggers a warning regarding single_user_login due<br>to an upstream configuration handling issue. Until this is<br>resolved upstream, the recommended approach is to use multi-user<br>mode, which functions correctly.
Under the Hood: The Dockerfile Strategy
The following sections detail how the<br>Dockerfile<br>achieves a robust, self-contained setup.
1. Defensive PHP Extension Installation
WebCalendar requires several PHP extensions: gd,<br>zip, intl, mbstring,<br>opcache, and SQLite support. Each extension depends<br>on specific system libraries (e.g., libpng-dev for<br>gd).
Rather than assuming which extensions are pre-installed in the<br>base<br>php:8-apache image, the Dockerfile checks the<br>runtime environment before installing:
(php -m | grep -qi "Zend OPcache" || docker-php-ext-install -j"$(nproc)" opcache) &&<br>(php -m | grep -qix pdo_sqlite || docker-php-ext-install pdo_sqlite) &&<br>(php -m | grep -qix sqlite3 || docker-php-ext-install sqlite3)
This approach ensures compatibility across different PHP 8.x<br>point releases, where bundling behaviors for extensions like<br>OPcache and SQLite may vary. It prevents build failures when the<br>base image updates its PHP version.
2. Shallow Git Clone
To resolve the upstream packaging issue, we clone directly from<br>git:
RUN cd / \<br>&& rm -rf /var/www/html \<br>&& git clone --depth 1 --branch "${WEBCALENDAR_REF}" \<br>https://github.com/craigk5n/webcalendar.git /var/www/html \<br>&& rm -rf /var/www/html/.git
Using --depth 1 performs a shallow clone, fetching<br>only the specific commit for the target tag. This keeps the<br>build fast and the final image size minimal. The<br>.git directory is removed afterward, as version<br>history is not needed in a production container.
3. Preparing the Installer
The WebCalendar install wizard writes configuration to<br>includes/settings.php. Since this file does not<br>exist in the fresh git clone, we create it and set appropriate<br>permissions:
RUN mkdir -p data \<br>&& touch includes/settings.php \<br>&& chmod 777 includes/settings.php \<br>&& chown -R www-data:www-data /var/www/html
4. The Bind-Mount Seeding Mechanism
This is the core innovation that allows us to keep the source<br>code inside the image while still using bind mounts for state.
The Dockerfile declares two volumes:
VOLUME ["/var/www/html/data", "/var/www/html/includes"]
When a host directory is bind-mounted to a container path, it<br>completely replaces the contents of that path in the image. If<br>we simply mounted an empty host folder to<br>/var/www/html/includes, we would lose all the<br>essential PHP include files cloned from git, breaking the<br>application.
The solution...