Skip to content

wincent/masochist

Repository files navigation

What

This repo contains the source code and content for my website at wincent.dev.

Content (stored on the content branch) is authored in plain-text-friendly markup formats like Markdown, written to static HTML output files on the public branch, and a small Rust application (built from the main branch) is used to handle dynamic parts of the site, such as search.

Note

While Masochist is mostly a static site generator, in its original form it was a dynamic stack built using React, Relay, and GraphQL. If you're interested in that, take a look at the main branch as it used to exist at commit b06dbb8488f88b3d.

Stack

  • Caddy: Web server.
  • Docker: Containers and orchestration.
  • Git: Content storage.
  • Lightning CSS: CSS minification.
  • Rust: Build and backend language.
  • Rocket: Backend web framework.
  • SWC: JavaScript minification.
  • dprint: Code and configuration formatting.

Supporting tools and technologies:

Prerequisites for local development

  • Rust toolchain (install via rustup or with brew install rust or similar).
  • Git (used during build and deployment; the system installed Git1 may be adequate, but I use Homebrew's Git, via brew install git).
  • Colima (brew install colima).
  • Docker (brew install docker docker-buildx docker-compose).
  • dprint (brew install dprint).

Local setup

The repo uses three branches, each with its own role:

  • main: Rust source code (this branch).
  • content: Source content (Markdown, images, static files).
  • public: Static build output (to be served by Caddy).

Set up worktrees for the other two branches:

bin/configure-worktrees

There are 3 remotes, configured with bin/configure-remotes:

  • origin: Canonical repo at git.wincent.dev.
  • github: Main mirror at github.com.
  • masochist: An Amazon EC2 instance where the actual site runs.

bin/ utilities

  • bin/build: Builds the static artifacts.
  • bin/check-format: Check formatting.
  • bin/check-links: Check for broken internal links.
  • bin/clippy: Run Clippy linter.
  • bin/configure-remotes: Configures origin, github, masochist and all remotes.
  • bin/configure-worktrees: Configures content and public worktrees.
  • bin/deploy: Deploys to EC2.
  • bin/dev: Runs Docker Compose locally.
  • bin/ecr: Builds and uploads container images to Amazon ECR (Elastic Container Registry)..
  • bin/format: Fix formatting.
  • bin/prod: Runs Docker Compose on remote host.
  • bin/pull: Fetches all remotes and updates main, content, and public checkouts.
  • bin/push: Pushes all branches (main, content, public) to all remotes (origin, github, masochist).

Running with Docker Compose (dev mode)

Install Docker and Colima (CLI alternative to docker-desktop cask):

brew install docker docker-buildx docker-compose colima

# Start Colima.
# --vm-type=vz: uses Apple's Virtualization Framework (faster on Apple silicon).
# --vz-rosetta: allows running x86_64 (Intel) container images; not needed but a useful default.
colima start --cpu 4 --memory 16 --vm-type=vz --vz-rosetta

# Confirm Colima is working.
docker ps
docker context ls # Should show Colima is current context (indicated with "*").
docker info
docker compose version
colima status

# Configure Colima to start at boot.
brew services start colima

# (Optional) For tools that expect standard docker.sock path, provide symlink as a convenience:
mkdir -p ~/.docker/run
ln -sf ~/.colima/default/docker.sock ~/.docker/run/docker.sock

Build the static site, then start the stack:

bin/build
bin/dev

This starts Caddy on https://localhost:2443 (self-signed cert) with the Rocket search server proxied behind it.

Running in a VM sandbox

For isolated development using Tart (to more safely run AI coding agents), VM management is handled by sb, a general-purpose sandbox tool installed via the wincent/wincent dotfiles repo. Project-specific configuration lives in .sandboxrc at the repo root.

Prerequisites

brew install sshpass cirruslabs/cli/tart

# Generate a long-lived OAuth token for Claude Code (valid 1 year):
claude setup-token
export CLAUDE_CODE_OAUTH_TOKEN=<token> # Add to shell profile.

# Alternatively if using Pi (or similar) use an API key:
export ANTHROPIC_API_KEY=<key> # Add to shell profile.

Creating and starting the VM

sb create # One-time: clones base image, provisions Docker + Rust, injects branches.
sb status # Check whether the VM is running and its IP.

Other lifecycle commands include: sb destroy, sb reset, sb restart, sb start, sb stop, and sb pull.

Running the service

sb ssh code/masochist/bin/dev

On the host, visit https://localhost:3443 (accept the self-signed cert). Port 3080 reaches the Rocket backend directly, bypassing Caddy.

Failure to run the service in the local development environment

If the service fails to start with:

runc run failed: unable to start container process: error during container init: unable to apply apparmor profile

the cause may be a corrupt /etc/apparmor.d/tunables/home.d/ubuntu file2; you can regenerate it and have Docker pick it up by running the following in the VM:

sudo dpkg-reconfigure apparmor
sudo systemctl restart docker

Moving code between host and VM

Push host branches into the VM:

sb inject         # Push all branches (main, content, public).
sb inject --force # Force-push (discard VM-only commits).

Pull VM changes back to the host:

sb extract # Show VM-only commits per branch.
sb apply   # Rebase and GPG-sign VM commits onto host branches.

After applying, verify and push:

cargo check && bin/test && bin/check-format && bin/clippy
bin/push # Push to production remotes (from host).

Run sb help for the full command list.

Server setup (EC2)

I set up the instance using Ansible, but the equivalent manual steps are as follows:

Docker

sudo dnf install docker -y
sudo systemctl enable docker
sudo systemctl start docker

# Add ec2-user so we can easily run Docker commands.
sudo usermod -aG docker ec2-user

# Add masochist user because our `bin/prod script runs as masochist,
# and needs to talk to Docker.
sudo usermod -aG masochist ec2-user

Docker Compose

sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

IAM role

The instance needs an IAM role with the AmazonEC2ContainerRegistryReadOnly policy attached.

ECR credential helper

Install the credential helper so Docker can authenticate with ECR automatically:

sudo dnf install amazon-ecr-credential-helper -y

Then configure Docker to use it:

mkdir -p ~/.docker
cat > ~/.docker/config.json << 'EOF'
{
  "credsStore": "ecr-login"
}
EOF

ECR repository

Create the repository (one-time).

Building the static site

As noted in the previously, you need Colima installed in order to complete the build.

bin/build

Deploying

aws login
aws ecr describe-registry --region us-east-1 --query registryId --output text
export ECR_ACCOUNT_ID=...

Push all branches, build and upload the Docker image, then deploy:

bin/push
bin/ecr build
bin/ecr upload
bin/deploy

Each step is independent and can be retried on failure.

Project structure

  • masochist-lib/: Shared library (content parsing, git interaction, indexing).
  • masochist-build/: Static site generator (depends on comrak for Markdown).
  • masochist-server/: Rocket search server (no Markdown rendering at runtime).
  • ops/: Caddyfile, Dockerfiles, Docker Compose configs.
  • bin/: Build, dev, and deploy scripts.
  • public/ (worktree): Public branch (build output, served by Caddy).
  • content/ (worktree): Content branch (Markdown, images, static files).

FAQ

Why the name "Masochist"?

See the introductory blog post, "Introducing Masochist".

Footnotes

  1. At the time of writing, macOS ships with Git v2.50.1, but Homebrew has v2.53.0.

  2. For some reason, I've seen this file corrupt with a series of trailing NUL bytes.

About

⛓ Website infrastructure for over-engineers

Topics

Resources

License

Stars

Watchers

Forks

Contributors