2016-02-19
A self-hosted website setup using LaTeX, Pandoc, Docker, and Git. Pages are plain HTML, version-controlled, and published via containers without dynamic components.
This document outlines the infrastructure and publishing method for
the static site hosted at eipi.dev. All infrastructure is
based on open tools and containers (Merkel, 2014). Each page is written
in LaTeX, converted to HTML using Pandoc (MacFarlane, 2006–2025), and
served through a containerized PHP webserver behind a Cloudflare Tunnel.
The entire stack is orchestrated with Docker (Docker Inc., 2025).
Each page was written as a .tex file in plain LaTeX,
with minimal metadata.
\title{Some Title} \date{2025-11-19} \begin{document} \maketitle ... \end{document}
Conversion was done using:
pandoc -s input.tex -o output.html
A bare Git repository was initialized in the shared container volume:
cd /var/www/html git init --bare .git
A post-receive hook was created to check out the working tree:
#!/bin/sh GIT_WORK_TREE=/var/www/html git checkout -f
The hook was saved to .git/hooks/post-receive and made
executable.
Key generation was done with:
ssh-keygen -t ed25519 -C "eipi.dev git access"
The public key was pasted into the PUBLIC_KEY
environment variable in the Compose file.
The file index.php automatically scans all numbered HTML
pages, extracts the <title> tag, and displays a
sorted list.
<?php $pages = glob("*.html"); usort($pages, function($a, $b) { return (int)$b - (int)$a; }); echo "<ul>\n"; foreach ($pages as $file) { if (!ctype_digit(pathinfo($file, PATHINFO_FILENAME))) continue; $contents = file_get_contents($file); if (preg_match("/<title>(.*?)<\/title>/i", $contents, $matches)) { $title = htmlspecialchars($matches[1]); echo " <li><a href=\"$file\">$title</a></li>\n"; } } echo "</ul>\n"; ?>
The result is a standard unordered list of links, sorted with the highest-numbered file first. Only files with numeric filenames are included.
To enable secure remote access, a Cloudflare Tunnel is used. This creates an outgoing encrypted connection from the local server to Cloudflare’s network. The tunnel client runs in a container with the command:
cloudflared tunnel run
A ‘TUNNEL_TOKEN‘ is passed via environment variable, which is
generated from the Cloudflare dashboard and associated with a
preconfigured tunnel. The domain (e.g. eipi.dev) is routed
through Cloudflare and linked to the tunnel endpoint.
cloudflared: image: cloudflare/cloudflared:latest command: tunnel run environment: - TUNNEL_TOKEN=YOUR_CLOUDFLARE_TUNNEL_TOKEN volumes: - tunneldata:/etc/cloudflared
This makes the entire container network accessible via Cloudflare, without exposing ports to the public internet.
SSH access is exposed through the same Cloudflare Tunnel. A specific
subdomain (e.g. git.eipi.dev) is routed to the SSH
container using Cloudflare’s configuration. This is done with:
cloudflared tunnel route dns <TUNNEL_ID> git.eipi.dev
And in the tunnel’s configuration file (e.g.
config.yml):
tunnel: <TUNNEL_ID> credentials-file: /etc/cloudflared/<TUNNEL_ID>.json ingress: - hostname: git.eipi.dev service: ssh://localhost:2222 - service: http_status:404
The SSH container is configured to listen on port 2222. Once routed, Git can push directly to:
git remote add origin ssh://[email protected]:/var/www/html
The SSH public key was added to the PUBLIC_KEY
environment variable in the Compose file, and access is authenticated
via key-based login.
All services were containerized with Docker Compose. Volumes were
shared across containers, with the website source mounted at
/var/www/html in both the webserver and SSH containers. The
following services were used:
php:8.2-apache
lscr.io/linuxserver/openssh-server
cloudflare/cloudflared
networks: eipi_net: services: web: image: php:8.2-apache networks: - eipi_net volumes: - webdata:/var/www/html restart: unless-stopped ssh: image: lscr.io/linuxserver/openssh-server:latest environment: - USER_NAME=git - PUBLIC_KEY=ssh-ed25519 AAAAC3... user@host networks: - eipi_net volumes: - sshdata:/config - webdata:/var/www/html restart: unless-stopped cloudflared: image: cloudflare/cloudflared:latest command: tunnel run networks: - eipi_net environment: - TUNNEL_TOKEN=YOUR_CLOUDFLARE_TUNNEL_TOKEN volumes: - tunneldata:/etc/cloudflared restart: unless-stopped volumes: webdata: sshdata: tunneldata:
Once the containers are running and the remote Git repository is accessible via the Cloudflare Tunnel, changes can be deployed using a standard Git push. From the local development machine:
git remote add origin ssh://[email protected]:/var/www/html git push origin main
This immediately triggers the post-receive hook on the server, which checks out the current commit to the working directory. As a result, updated HTML files are reflected live on the website without further processing.
The repository is configured to ignore .tex source files
using a .gitignore entry. This ensures that only the
generated .html documents are deployed:
*.tex
This separation keeps the source and published formats distinct and avoids uploading raw LaTeX source to the production site.
All pushes to the repository are instantly reflected on the website without a build step or deployment. The working tree is live, and changes are applied in real time.
The system avoids all forms of templating engines, CMSs, or scripting beyond PHP for listing files. The site is plain HTML, generated from LaTeX and Pandoc, with version control via Git. Updates occur only when content is worth preserving.
Docker Inc. (2025). Docker: Empowering App Development for Developers. Retrieved from https://www.docker.com
MacFarlane, J. (2006–2025). Pandoc: A Universal Document Converter. Retrieved from https://pandoc.org
Merkel, D. (2014). Linux containers. Linux Journal, 2014(239).