Skip to content
Petri-HubPublic

About

🧰 A homelab running on a laptop, managed entirely through Terraform and Docker Compose.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

157 Commits

Folders and files

Repository files navigation

🧰 lab


A homelab running on a single laptop.
Infrastructure and services managed entirely as code

Terraform validation Last commit Time spent


About

TL;DR: the server that sits at home and runs my game servers, backups and a few tools I use every day. Terraform manages Cloudflare and Tailscale, Docker Compose manages every service, and nothing is reachable without going through one of the two.

Hardware

MachineVAIO laptop (VJFE59F11X)
CPUAMD Ryzen 7 5700U, 8 cores / 16 threads
Memory16 GB
Storage512 GB NVMe SSD
OSUbuntu 26.04 LTS
BackupsCloudflare R2 bucket, where Restic keeps the snapshots

Limited hardware forces some creativity. Restic deduplicates every snapshot, so backups every 20 minutes stay inside R2's free tier, and services are picked for doing one thing well on little memory: most containers here use less than 64 MB.

Architecture

Lab Architecture Diagram

Access flows

Path Description
Internet Users access services through Cloudflare. Cloudflare checks the user's email and sends a one-time code to verify them. Once verified, traffic goes through the tunnel into the server, where NGINX sends each request to the right service. A few specific paths skip the email check, like the Wakapi API that my GitHub profile reads from, and the Gatus status page is public.
Tailscale Devices connected to the Tailscale network can access services directly with randomized ports. The server only allows certain ports for laptops and phones, and shared users can only reach some services.

Philosophy

Infrastructure as Code.
Everything that can be codified, is. Terraform manages DNS, the tunnel, access policies and network ACLs, and Docker Compose defines every service. There are no SSH-and-pray moments.

Defense in depth.
Public traffic only enters through a Cloudflare Tunnel with Cloudflare Access in front of it, private traffic goes through Tailscale with ACLs, and host-exposed services use non-default ports. Each layer holds on its own.

Predictable under load.
Containers are grouped as infrastructure, applications and sandboxes, and each has CPU and memory limits so one workload cannot starve the others.

Services

Infrastructure

Β Β Β Β Β  Service Used for
cloudflared Connects the server to Cloudflare's edge network, routing public traffic through a secure tunnel so services are accessible without exposing the server directly
nginx Acts as a reverse proxy, reading the incoming domain and forwarding each request to the correct internal service
gatus Checks every running service every minute and shows which ones are up on a public status page
btop Exposes a real-time system monitor through the browser so you can check CPU, memory, and processes without SSH
filebrowser Provides a web-based file manager to browse, upload, and edit files across the entire server
ofelia Runs scheduled jobs inside containers using Docker labels, used here to trigger backups automatically
rclone Runs as a REST server that receives and stores backup data from Restic
restic Backs up the Satisfactory saves, the Wakapi and ai-memory data, Hermes' data, Lemon's sandbox and the Terraform state to the Rclone server on a schedule
upsnap Wakes my main PC with Wake-on-LAN, since it lives outside the lab, and shows which machines on the network are online

Applications

Β Β Β Β Β  Service Used for
satisfactory Runs a dedicated Satisfactory game server that friends can join at any time, with automatic backups and configurable player limits
palworld Runs a dedicated Palworld server for friends, with its saves backed up automatically
dst Runs a Don't Starve Together dedicated server cluster (Master + Caves shards) that friends can join over Tailscale
ai-memory Runs a shared long-term memory server that coding agents (Claude Code, OpenCode, ...) across multiple machines connect to over MCP, so context and handoffs persist between sessions and agents
teamspeak Runs a self-hosted TeamSpeak 6 voice server so friends can join a private voice channel over Tailscale
wakapi Collects coding activity from the editors and terminals on my machines, a self-hosted alternative to WakaTime that also feeds my GitHub profile. Separate wakapi-pepper, wakapi-mint and wakapi-lemon jobs report each agent's Hermes activity to its own Wakapi user every 5 minutes
ytdlp Provides a web interface for yt-dlp to download videos from various platforms directly to the server
kamiyomu Runs a self-hosted, extensible manga reader that discovers, downloads, and organizes manga from various sources into a personal library
kavita Serves a polished web-based reader over the manga KamiYomu downloads, with library management, metadata, and per-user reading progress

Sandboxes

Β Β Β Β Β  Sandbox Used for
lemon-sandbox Gives Lemon a persistent SSH workspace for multiple Roblox repositories, with each project's tools installed through Rokit

Template

services/
└── {infra|apps}/
    └── <service-name>/
        β”œβ”€β”€ configuration/        # Static config files mounted into the container
        β”œβ”€β”€ .env                  # Local environment values (gitignored)
        β”œβ”€β”€ .env.example          # Documented variables with placeholder values
        └── compose.yml           # Service definition with ports, volumes, networks, resources
sandboxes/
β”œβ”€β”€ compose.yml                   # Includes each sandbox
└── <sandbox-name>/
    β”œβ”€β”€ .env                      # Local environment values (gitignored)
    β”œβ”€β”€ .env.example              # Documented variables with placeholder values
    β”œβ”€β”€ compose.yml               # Sandbox image, storage, network and limits
    └── README.md                 # Setup and runtime mounts

Policies

Keep configuration in a dedicated folder

Static config files live in a configuration/ subdirectory, so there is a single place to look for a service's settings.

# services/infra/nginx/compose.yml

volumes:
  - ./configuration/nginx.conf.template:/etc/nginx/templates/nginx.conf.template:ro

Prefix environment variables with the service name

Prefixes avoid collisions and make it clear what each variable controls.

# services/apps/satisfactory/.env.example

SATISFACTORY_MAX_PLAYERS=4
SATISFACTORY_GAME_PORT=7328
SATISFACTORY_MESSAGING_PORT=1273

Separate infra, apps and sandboxes

Infrastructure keeps the server running and observable; applications are hosted tools and games. Sandboxes are persistent development environments used by agents.

services/
β”œβ”€β”€ infra/       # monitoring, networking, backups, access
β”‚   β”œβ”€β”€ btop/
β”‚   β”œβ”€β”€ cloudflared/
β”‚   β”œβ”€β”€ dozzle/
β”‚   β”œβ”€β”€ filebrowser/
β”‚   β”œβ”€β”€ nginx/
β”‚   β”œβ”€β”€ ofelia/
β”‚   β”œβ”€β”€ rclone/
β”‚   β”œβ”€β”€ restic/
β”‚   └── upsnap/
└── apps/        # game servers and tools
    β”œβ”€β”€ dst/
    β”œβ”€β”€ kamiyomu/
    β”œβ”€β”€ kavita/
    β”œβ”€β”€ palworld/
    β”œβ”€β”€ satisfactory/
    β”œβ”€β”€ teamspeak/
    β”œβ”€β”€ wakapi/
    └── ytdlp/
sandboxes/        # agent development environments
β”œβ”€β”€ compose.yml
└── lemon-sandbox/

Set resource limits on every container

# services/infra/dozzle/compose.yml

deploy:
  resources:
    limits:
      cpus: '0.5'
      memory: 64M

Use environment variables for port randomization

Only the port exposed on the host is randomized; internal ports stay at their well-known defaults.

# services/infra/btop/compose.yml

ports:
  - "${BTOP_PORT}:7681"

Connect every service to the same network

Services and sandboxes resolve each other by container name on a shared Docker network. The exception is UpSnap, which needs the host network to send Wake-on-LAN packets.

# shared pattern across all compose.yml files

networks:
  lab:
    name: lab
    external: true

Container conventions

Containers restart on their own after a crash or a reboot, and their names match the Compose service key.

# shared pattern across all compose.yml files

restart: unless-stopped
container_name: <service-name>

Technologies

Ubuntu
Ubuntu
Docker
Docker
Terraform
Terraform
Cloudflare
Cloudflare
Tailscale
Tailscale
NGINX
NGINX
GitHub Actions
GitHub Actions

Roadmap

What's already in place:

  • βœ… Every service in Docker Compose, split into infra and apps, each with CPU and memory limits
  • βœ… Tailscale ACLs and device tags, managed with Terraform
  • βœ… A Cloudflare Tunnel behind Access, replacing ngrok
  • βœ… btop in the browser, replacing Glances
  • βœ… UpSnap, to wake my main PC over Wake-on-LAN
  • βœ… Game servers for friends: Satisfactory, Palworld and Don't Starve Together
  • βœ… A TeamSpeak 6 voice server, over Tailscale
  • βœ… Tailscale Services for the TCP-only ports
  • βœ… Terraform validation on every PR, and a pre-commit hook
  • βœ… A manga library: KamiYomu to download, Kavita to read
  • βœ… Hermes in a container of its own, running Pepper
  • βœ… Wakapi for my coding time across machines, with a user for Pepper
  • βœ… ai-memory, a long-term memory shared by every coding agent I use
  • βœ… Container limits sized from measured peaks
  • βœ… Restic backups to a private R2 bucket
  • βœ… Gatus, a public status page checking every service, replacing Dozzle

Getting started

Prerequisites

Setup

# 1. Install the local tooling
make install

# 2. Create the shared Docker network
make setup

# 3. Bootstrap Terraform
make terraform-init

# 4. Review and apply infrastructure changes
make terraform-plan
make terraform-apply

# 5. Start infrastructure services
make infra-up

# 6. Start application services
make apps-up

# 7. Start prepared agent sandboxes
make sandboxes-up

Commands

Command Description
install Install pipx and the pre-commit hooks
setup Create the lab Docker network
infra-up Start all infrastructure services
infra-up-build Start infra services with rebuild
infra-down Stop all infrastructure services
apps-up Start all application services
apps-up-build Start app services with rebuild
apps-down Stop all application services
sandboxes-up Start all prepared agent sandboxes
sandboxes-down Stop all agent sandboxes
terraform-init Initialize Terraform
terraform-init-upgrade Re-initialize with provider upgrades
terraform-plan Preview infrastructure changes
terraform-apply Apply infrastructure changes

About

🧰 A homelab running on a laptop, managed entirely through Terraform and Docker Compose.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Used by

Contributors

Languages