Docker Compose recipes for homelab: reproducible stacks for your PKM, not just services

A practical deep dive into Docker Compose recipes for homelab — real examples, comparisons, and setup guides.

Docker Compose recipes for homelab: reproducible stacks for your PKM, not just services

Docker Compose recipes for homelab: reproducible stacks for your PKM, not just services

A news item on HN about Karpathy’s Pelican sparked a simple realization for me: people crave portable, maintainable knowledge assets. Pelican is a static blog engine, but the broader impulse is clear—make your knowledge and tooling portable, easily reproducible, and shareable. In my homelab, that same impulse shows up as reproducible stacks you can recreate on a whim. Docker Compose is the glue that makes this practical: you can codify a complete home stack—proxy, PKM apps, dashboards, and backups—into a single file or a handful of files that you can ship to a new machine in minutes.

In this post, I’ll walk through practical Docker Compose recipes you can use in a homelab to host a small PKM-friendly stack (notes, a wiki, and a light note-taking front-end), plus a few general-purpose home services. I’ll anchor the discussion to the idea of knowledge management and trust in tooling that the recent news items highlight, and I’ll give you concrete commands, a ready-to-tailor compose file, and tips for keeping things sane over time.

Why Compose matters for homelab knowledge work

A lot of homelabs are about control and portability. You want to:

  • Keep your data in named volumes so you can back up, restore, and migrate easily.
  • Rebuild or migrate stacks without redeveloping your deployment logic.
  • Separate concerns (proxy, application, database) for security and upgrade paths.
  • Manage secrets responsibly without committing them to a repo.

The Karpathy Pelican thread and the broader PKM discourse matter here because the value of a homelab isn’t just uptime; it’s your ability to capture, organize, and retrieve knowledge (notes, configurations, how-tos) with integrity. Docker Compose gives you a way to codify that architectural discipline: a single manifest you can version, share, and bootstrap.

What changed in practice—and what you should do next

  • The mental model shift: Instead of a pile of manual steps, you keep an up-to-date YAML chart of your stack. This aligns with the “note-taking and PKM” trend: your infrastructure becomes a knowledge artifact you can annotate, extend, and port to new hosts.
  • The tooling shift: Compose v2 (integrated into the Docker CLI as docker compose) makes it easier to treat multi-service stacks like code. It’s not just about running containers fast; it’s about reproducibility, safety, and gradual upgrades.
  • The trust angle: Tools encode trust. If you rely on a well-documented compose file, you’re siding with predictable behavior and forward compatibility, not ad hoc scripts.

Recipe in brief: a practical, minimal Compose file for a PKM-friendly homelab

The stack I’ll walk through includes:
- A reverse proxy (Traefik) with automatic TLS via Let’s Encrypt
- A light PKM app (Trilium Notes) for personal knowledge capture
- A wiki (BookStack) to capture project docs and workflows
- A database (MariaDB for BookStack)
- A simple admin tool (Adminer) for quick DB introspection

All of this is designed to be disabled or enabled via profiles, so you can tailor to your needs.

Code: docker-compose.yaml (example)

version: "3.9"

services:
  traefik:
    image: traefik:v2.12
    container_name: traefik
    command:
      - --providers.docker
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
      - --certificatesresolvers.myresolver.acme.email=you@example.com
      - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik-letsencrypt:/letsencrypt
    networks:
      - web
    restart: always
    environment:
      - TZ=UTC

  mariadb:
    image: mariadb:11
    container_name: bookstack_db
    env_file: .env
    environment:
      - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
      - MYSQL_DATABASE=${BOOKSTACK_DB}
      - MYSQL_USER=${BOOKSTACK_USER}
      - MYSQL_PASSWORD=${BOOKSTACK_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - web
    restart: always
    secrets:
      - db_root_password

  bookstack:
    image: ghcr.io/linuxserver/bookstack:latest
    container_name: bookstack
    depends_on:
      - mariadb
    environment:
      - db_host=mariadb
      - db_user=${BOOKSTACK_USER}
      - db_pass=${BOOKSTACK_PASSWORD}
      - db_name=${BOOKSTACK_DB}
    ports:
      - "8080:80"
    volumes:
      - bookstack_data:/config
    networks:
      - web
    restart: always
    env_file: .env
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.bookstack.rule=Host(`notes.yourdomain.example`)"
      - "traefik.http.routers.bookstack.entrypoints=websecure"
      - "traefik.http.routers.bookstack.tls=true"

  trilium:
    image: zadam/trilium:0.6.0
    container_name: trilium
    depends_on:
      - mariadb
    volumes:
      - trilium_data:/root/.config
    environment:
      - TS_ENDPOINT=http://notes.yourdomain.example
    networks:
      - web
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.trilium.rule=Host(`notes.yourdomain.example`) && PathPrefix(`/notes`)"
      - "traefik.http.routers.trilium.entrypoints=websecure"
      - "traefik.http.routers.trilium.tls=true"
    restart: unless-stopped

  adminer:
    image: adminer
    container_name: adminer
    depends_on:
      - mariadb
    ports:
      - "8081:8080"
    networks:
      - web
    restart: always
    labels:
      - "traefik.enable=false"

volumes:
  traefik-letsencrypt:
  db_data:
  bookstack_data:
  trilium_data:

networks:
  web:
    driver: bridge

secrets:
  db_root_password:
    file: ./secrets/db_root_password.txt

Notes on the compose file:
- Use .env to hold non-secret values (e.g., BOOKSTACK_DB, BOOKSTACK_USER, BOOKSTACK_PASSWORD). Secrets section is wired for a root password, but you can simplify if you’re just exploring.
- The labels show how to expose BookStack and Trilium behind Traefik with TLS. Replace notes.yourdomain.example with your actual domain and ensure DNS points to your homelab.
- Profiles: If you want to gate features, you can split services into profiles and enable with docker compose --profile. For example:
- profiles:
- pkm for Trilium
- web for Traefik and external access
You can adapt the YAML to include:

services:
  traefik:
    profiles: ["web"]
  bookstack:
    profiles: ["pkm", "web"]
  trilium:
    profiles: ["pkm"]
  adminer:
    profiles: ["dbtools"]

How to run it (practical commands)

  • Create a minimal environment file (.env) and optional secrets directory. Then bring the stack up:
  • docker compose --project-name homelab-pkm up -d --build
  • If you’re using profiles:
  • docker compose --profile web --profile pkm up -d
  • Tidy commands to manage the lifecycle:
  • docker compose pull
  • docker compose down
  • docker compose logs -f
  • docker compose restart

Backup and restore basics

  • Data persistence is via named volumes (db_data, bookstack_data, trilium_data). For a quick dump of BookStack’s DB:
  • docker exec -i bookstack_db mysqldump -u root -p$DB_ROOT_PASSWORD bookstack > bookstack_backup.sql
  • Restore:
  • docker exec -i bookstack_db mysql -u root -p$DB_ROOT_PASSWORD bookstack < bookstack_backup.sql

Security and reliability tips

  • Secrets management: store sensitive values outside the compose file and pass them via a secrets store or environment management tool. If you’re not using swarm secrets, at least keep a separate secrets/ directory with .gitignore rules.
  • Least privilege: run Traefik and apps with minimal privileges and restrict container network exposure. Let Traefik handle TLS termination; don’t expose app ports directly to the wild internet.
  • Backups are part of your PKM stack. Treat the database backups of BookStack and Trilium as part of your knowledge artifacts; store them with your other notes in a separate offsite if possible.
  • Image hygiene: pin to specific tags and consider digest pinning for critical services. Automate image refresh with care (e.g., nightly pull + test).
  • Observability: add a simple Prometheus node_exporter or cAdvisor later if you want quick dashboards, but for a home setup keep it lean at first.

A small “PKM stack” design pattern you can reuse

  • Rules of thumb:
  • Data first: volumes persist your notes, journal entries, and docs.
  • Front door: Traefik gives you a single TLS-terminating front, with host-based routing to your own domain. This also makes it trivial to add more services later.
  • Knowledge as code: you can keep a companion repo with your own compose files, annotated by README updates that describe changes to your PKM stack. This is the exact kind of artifact that mirrors the PKM habit in real life.
  • Modular expansion:
  • Add a Wiki.js instance behind the same Traefik proxy for broader project docs.
  • Integrate a small dashboard (portainer, perhaps) to manage containers if you’re pivoting to a more formal ops mindset.
  • Add a lightweight notes front-end (Neovim or a browser-based note app) as a microservice, so your knowledge remains accessible from multiple devices.

A quick comparison: Compose options you might consider

Tool / option What it is Pros Cons When to use
Docker Compose (v2 via docker compose) Official multi-service orchestration in Docker CLI Tight integration with Docker Engine, simple to learn; profiles for modularity Some folks hit feature gaps for complex deployments Quick homelab stacks; you want a familiar workflow
Standalone docker-compose (v1) Python-based tool, older format Wide compatibility with older projects Becoming deprecated; lacks newer CLI conveniences Maintaining legacy stacks; learning history
Podman Compose Podman-based compose tool Rootless by design; daemonless; OCI-friendly Some community gaps with exact Docker equivalence Secure, rootless homelabs or Linux-only environments
Kubernetes (k3s) with Helm Full cluster orchestration Extreme scalability; mature ecosystem More complexity; steeper learning curve Large homelabs or when you’re migrating toward production-grade semantics

Note that for most homelabs, Docker Compose (v2) is the quickest path to predictable, reproducible runs. Podman Compose becomes compelling if you’re aiming for rootless operation. Kubernetes is excellent when you’re intentionally scaling or want to practice production-grade patterns, but it’s not necessary for a PKM-centered homelab.

What changed in practice for you as a reader?

  • You don’t need a mountain of separate install steps anymore. A single compose file, a domain, and a few environment variables will spin up a tiny PKM stack that you can back up or move.
  • The approach lines up with your PKM habits. Your note-taking, wiki, and project docs become part of the same lifecycle you use to bootstrap your homelab, not a separate “tools for notes” worry.
  • Tools you trust come from their predictability and the ability to version-control them. If you’ve read “Developers are attached to tools because tools encode trust,” you’ll recognize you’re doing precisely that with your Compose recipes—treating the stack as a trusted, revisable artifact.

Two practical enhancements to consider next

  • Add a simple CI trigger: push your compose file to a Git repo; set a GitHub Action (or similar) to run docker compose pull and docker compose up -d on a tag or on merge to main. You now have a reproducible upgrade path without manual steps.
  • Introduce a basic PKM workflow: add a small automation that exports notes to a portable format (markdown) and stores them as a companion backup. For example, a scheduled job could export Trilium notes or Wiki.js content into a shared backup volume or a separate repository.

A practical example workflow

  • Create your compose file in a versioned directory (git init; add; commit).
  • Add a .env with basic values:
  • DB_ROOT_PASSWORD=your-secure-root
  • BOOKSTACK_USER=bookstack
  • BOOKSTACK_PASSWORD=your-secure-password
  • BOOKSTACK_DB=bookstack
  • Start:
  • docker compose --profile web --profile pkm up -d --build
  • Add a new service (e.g., a small dashboard) by editing docker-compose.yaml and adding a new service stanza. Then:
  • docker compose up -d
  • docker compose ps
  • Periodically:
  • docker compose pull
  • docker system prune -f (with caution)

Final thoughts: make it yours, then share it

The point isn’t to chase every new item in the tech press. It’s to adopt a disciplined pattern for your homelab—treating your environment as a knowledge artifact, codified, portable, and testable. The Karpathy Pelican signal isn’t just about blogs; it’s about the broader habit of making personal knowledge artifacts that survive machines, networks, and upgrades. Docker Compose lets you embed that habit in your infrastructure.

Actionable conclusion

Start with one tiny, real use-case: run Traefik as a proxy and Trilium Notes as your first PKM app behind it. Get a single-domain DNS, a certificate, and a compose file you can version-control. Then incrementally add BookStack or Wiki.js as your second module, managed behind the same proxy. In two evenings you’ll have a stable, portable stack you can clone to another machine or share with a collaborator, and you’ll have turned your homelab into a practical PKM engine you can trust—just like the tools you rely on to capture knowledge in the first place.


Backup

Product Notes Link
Backblaze B2 Affordable offsite object storage Link
Wasabi Affordable offsite object storage Link