# Jason Peters I work across practical websites, software development, Linux infrastructure, and technical experiments. This site collects technical writing, project updates, graphic work, and experiments from that practice. ## Topics ### Web Development URL: https://jasonjpeters.com/topics/web-development/ Articles and development notes covering practical web development, application structure, tooling, and maintainable project workflows. ### Linux URL: https://jasonjpeters.com/topics/linux/ Linux articles and notes about desktop workflows, servers, command-line tooling, and development environments. ## Public Technical Content ### The Pipeline Pattern in JavaScript URL: https://jasonjpeters.com/posts/the-pipeline-pattern-in-javascript/ Description: Use a JavaScript pipeline pattern to process large data workflows with small, composable task classes inspired by Laravel middleware. Published: 2026-08-14 Tags: JavaScript, Laravel, WordPress, Web Development, Software Development, pipeline, data migration Topics: javascript, laravel, wordpress, web-development, software-development, pipeline-patterns, data-migration When you are processing a large amount of data, the work usually stops being a single clean operation pretty quickly. Migrating an MSSQL database with millions of articles, consuming articles from an RSS feed, normalizing that content, and then inserting it into a MySQL database for WordPress (or other applications) all involve a chain of smaller steps that need to happen in a predictable order. That kind of workflow can become difficult to maintain when every step is packed into one long script. One import might need HTML cleanup, another might need category mapping, another might need duplicate detection, and another might need custom image handling. The more the application grows, the more useful it becomes to break the process into small, focused tasks that can be added, removed, or reordered. I liked the structure of Laravel's pipelines, especially how middleware can pass a request through a series of classes where each class handles one concern. This JavaScript version follows the same general idea: define a common task interface, register the available tasks, and compose different pipelines depending on what the application needs. The result is a pattern that can adapt across projects. A migration script, an RSS importer, a cleanup utility, or a publishing workflow can all share the same pipeline runner while using different task lists. The examples below show one way to structure that pattern in Node.js. They start with a task registry that loads available task classes from a directory, then define a pipeline runner that executes those tasks in order, followed by a basic task class and an application entry point that decides which tasks belong in the workflow. The task registry is responsible for discovering every task module in the `pipeline/tasks` directory and exposing them by filename. It reads the directory synchronously to find JavaScript files, imports each file asynchronously with a file URL, stores each default export in a plain object, and exports the completed registry after loading finishes. ```javascript // pipeline/tasks.js import fs from 'fs'; import path from 'path'; import { fileURLToPath, pathToFileURL } from 'url'; class Tasks { constructor() { this.tasks = {}; } async load() { const dir = this.getDirname(import.meta.url) + '/tasks'; try { const files = fs .readdirSync(dir) .filter((file) => file.endsWith('.js')); await Promise.all(files.map((file) => this.importModule(file, dir))); } catch (error) { console.error(`Failed to load tasks:`, error); } return this.tasks; } async importModule(file, dir) { const modulePath = path.join(dir, file); const moduleName = path.basename(file, '.js'); try { const module = await import(pathToFileURL(modulePath).href); this.tasks[moduleName] = module.default; } catch (error) { console.error(`Failed to import ${moduleName}:`, error); } } getTask(name) { return this.tasks[name]; } getDirname(url) { return path.dirname(fileURLToPath(url)); } } const task = await new Tasks().load(); export default task; ``` The pipeline class receives an ordered list of task constructors and validates that each entry can be instantiated. Its `run` method passes a shared `context` object through each task in sequence, replacing the context with the result of every `execute` call so each task can transform or enrich the data before the next task runs. ```javascript // pipeline/pipeline.js export default class Pipeline { constructor(tasks) { this.tasks = tasks.map((task) => typeof task === 'function' ? task : this.functionError('Task is not a constructor', task) ); } async run(context) { for (const Task of this.tasks) { const task = new Task(); if (typeof task.execute !== 'function') { this.functionError('Task missing execute method', task); } context = await task.execute(context); } return context; } functionError(message, task) { console.error(message, task); throw new Error(message); } } ``` Each task is a small class with a standard `execute` method. The pipeline only depends on this method contract, so new task files can be added as the application grows, and old ones can be removed without changing the pipeline runner itself. ```javascript // pipeline/tasks/task.js export default class Task { async execute(context) { // Task logic goes here return context; } } ``` The application code chooses which registered task classes should run, builds a pipeline from that ordered list, and executes the pipeline once for each item in the input data. As the workflow changes, tasks can be added, removed, or reordered in this array without changing the task classes or the pipeline runner. ```javascript // app.js import Pipeline from './pipeline/pipeline.js'; import task from './pipeline/tasks.js'; const processData = async (data) => { const tasks = [ task.task, // Add or remove task classes here as your application needs grow. // task.validateInput, // task.saveResult, ]; for await (let item of data) { const pipeline = new Pipeline(tasks); await pipeline.run(item); } } await processData(data); ``` This pattern is useful because it keeps each step small while still making the overall workflow easy to understand. Whether the job is migrating millions of records, importing articles from a feed, or building a custom publishing process, the pipeline becomes the stable structure around changing application needs. When a new requirement appears, you can usually add another task, reorder the list, or remove a step without rewriting the entire process. ### Development Environments, Local Clouds, and Distributed Systems with Incus URL: https://jasonjpeters.com/posts/development-environments-local-clouds-and-distributed-systems-with-incus/ Description: Modern web development is rarely a single app anymore - it's a constellation of services. Published: 2025-10-13 Tags: linux, debian, virtualization, development, homelab Topics: linux, virtualization, web-development, infrastructure When we're building modern web projects together, we're rarely dealing with a single app or service. Whether it's my laptop or primary workstation, the setup usually looks like a constellation of services: web servers, APIs, databases, caches, queues, and background workers all trying to mimic production. Docker and VirtualBox each help, but they live at opposite ends of what we need. Docker gives us containers. VirtualBox gives us VMs. **Incus** gives us both—unified, scriptable, and resource-efficient. This is the workflow I rely on, and we'll walk through how to stand up a local cloud side by side so we can develop and test the whole stack without leaving our desks. ## Incus vs Docker, VirtualBox, and Friends Here's how I frame the current tooling landscape when we're sizing up our local cloud experiments: | Tool | Best For | Weakness | |------|----------|----------| | Docker | Packaging and deploying individual services | Hard to simulate full systems or custom networks | | VirtualBox | Full desktop virtualization | Heavy and manual for complex topologies | | Proxmox VE | Server-grade virtualization and clustering | GUI-centric, heavier setup | | Incus | Local clusters of containers + VMs | CLI-centric (no default GUI) | Docker is great for shipping applications. Incus is for simulating infrastructure. We can even run Docker inside an Incus container or VM — handy when we want to test how containerized workloads behave in different environments. Incus is a **next-generation container and virtual machine manager** that lets us build full-stack distributed systems locally. I love how it matches the way I reason about projects: we can simulate production environments, test networked services, isolate client work, or experiment with infrastructure patterns — all without spinning up real cloud VMs. ## History of Incus Incus has roots in **LXD**, Canonical’s container hypervisor built on **LXC (Linux Containers)**. LXD provided a clean API and CLI to manage both containers and VMs. It became beloved among developers who wanted lightweight, full-system containers that felt like mini-VMs. In 2023, Canonical decided to internalize LXD’s development, removing it from the open Linux Containers project. The original maintainers — the same engineers who built LXD in the first place — forked it into a new project (https://linuxcontainers.org/incus/announcement/) called **Incus**. Today, **Incus** lives under linuxcontainers.org (https://linuxcontainers.org/incus), fully open and community-governed, free from corporate oversight. Think of it as: > LXD, but freer — and faster to evolve. ## Why Web Developers Should Care Incus isn’t just a sysadmin’s toy — it’s an incredible tool for **application developers** like us. Whenever we want to replicate a full production environment locally, complete with multiple hosts, private networks, and mixed OS types, Incus makes that almost trivial. I treat it as my rehearsal stage and pull you into the same mindset: the more we automate this, the easier collaboration becomes. **Here’s what it brings to our shared workflow:** - **Real operating systems** in containers — not just minimal app images. - **Virtual machines** for kernel-level or distro-specific testing. - **Unified management** for both containers and VMs through one CLI or REST API. - **Native networking** to model internal and external networks realistically. - **Profiles and projects** to isolate different applications or stacks. In short: Incus turns our machine (or a small server) into our own **mini data center**. ## Containers vs VMs in Incus Incus supports two types of instances: | Type | Ideal Use Case | Characteristics | |------|----------------|-----------------| | **System Container** | Lightweight services — web apps, databases, caches | Shares host kernel, near-native speed | | **Virtual Machine** | Testing alternate OSes or kernel features | Full isolation via KVM/QEMU | The magic lies in **unification**: containers and VMs use the same commands, same configuration, same network. We can start with a container-based stack, then swap one component into a VM without changing any muscle memory. When I reach for a different instance type, it’s because I reasoned through the trade-offs, and we can make the same call together on the fly. ## Install and Configure Incus **Incus** is available for a number of different Linux distributions (https://linuxcontainers.org/incus/docs/main/installing/#install-incus-from-a-package) such as Fedora, Ubuntu, Void Linux, and others. I run it on Debian 13 Trixie (https://www.debian.org/), and I lean on Zabbly's Debian packages (https://github.com/zabbly/incus?tab=readme-ov-file#availability) to stay current. Let’s go through the exact steps together so you can mirror the setup or adapt it to your distro. ### Zabbly Repository Setup We start by giving APT access to the up-to-date package builds (Stable branch). That means creating a dedicated keyring directory, importing Zabbly’s signing key, and writing a `.sources` file so Debian knows about the new repository. Once the repo is registered, `apt update` refreshes the package index to include Incus. ```bash terminal copy title="Install Zabbly Repository" prompt="as root" mkdir -p /etc/apt/keyrings curl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc sh -c 'cat < /etc/apt/sources.list.d/zabbly-incus-stable.sources Enabled: yes Types: deb URIs: https://pkgs.zabbly.com/incus/stable Suites: $(. /etc/os-release && echo ${VERSION_CODENAME}) Components: main Architectures: $(dpkg --print-architecture) Signed-By: /etc/apt/keyrings/zabbly.asc EOF' apt update ``` With the repository configured, we install Incus proper. Adding our user to the `incus-admin` group lets us control Incus without running each command as root, and enabling the service ensures the daemon starts automatically after a reboot. ```bash terminal copy title="Install and Configure Incus" prompt="as user" sudo apt install incus sudo usermod -aG incus-admin $USER sudo systemctl enable --now incus ``` Before we launch anything, we confirm the UID and GID mappings that Incus uses for unprivileged containers. These ranges let Incus safely translate container IDs into host IDs. ```bash terminal copy title="Verify ID Mapping" prompt="as user" cat /etc/sub{g,u}id ``` ```sh # /etc/subgid :100000:65536 root:1000000:1000000000 # /etc/subuid :100000:65536 root:1000000:1000000000 ``` If the above command is missing `root:1000000:1000000000` in the output, we run: ```bash terminal copy title="Unprivileged Containers" prompt="as user" echo "root:1000000:1000000000" | sudo tee -a /etc/subuid /etc/subgid ``` This gives Incus permission to translate a very large range of UIDs and GIDs for use inside unprivileged containers, keeping them secure even if they think they are running as root. After that we reboot the machine and initialize Incus: ```bash terminal copy title="Initialize Incus" prompt="as user" incus admin init ``` The initializer can create a managed bridge automatically, but I like to tweak it afterward. To give us an easy-to-remember subnet, we edit the bridge and assign it a 10.10.10.0/24 network. ```bash terminal copy title="Set IPv4" prompt="as user" incus network edit incusbr0 ``` ```yaml config: ipv4.address: 10.10.10.1/24 ... ``` With the bridge configured, we can launch containers or VMs, assign them static addresses, and stitch together the services that make up our local cloud. It feels just like production—only everything lives on the laptop sitting in front of us. Anytime I pause to make a networking decision, it’s so we stay aligned on how traffic should flow. ```bash terminal copy title="Launch your first container" prompt="as user" incus launch images:alpine/edge db incus ls ``` ``` +------+---------+--------------------+------+-----------+-----------+ | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | +------+---------+--------------------+------+-----------+-----------+ | db | RUNNING | 10.10.10.10 (eth0) | | CONTAINER | 0 | +------+---------+--------------------+------+-----------+-----------+ ``` ### Helper Scripts I Keep Around Below are a couple we can lean on together. They’re simple, but they save me time and reduce copy-paste mistakes when we’re iterating fast. - `incus-static-ip`: quickly pins a container to a specific IP on our bridge. I use it whenever I am wiring services together and don’t want their addresses to drift. ```bash terminal copy title="/usr/local/bin/incus-static-ip" prompt="" #!/usr/bin/env bash read -p "Container Name: " name read -p "IP Address: " ip incus stop "$name" incus network attach incusbr0 "$name" eth0 eth0 incus config device set "$name" eth0 ipv4.address "$ip" incus start "$name" ``` - `incus-bind-dir`: mounts a host directory into a container so I can edit files locally and run them inside the instance. ```bash terminal copy title="/usr/local/bin/incus-bind-dir" prompt="" #!/usr/bin/env bash read -p "Container Name: " name read -p "Device Name: " device read -p "Host Directory: " hdir read -p "Container Directory: " cdir incus config device add "$name" "$device" disk source="$hdir" path="$cdir" shift=true ``` ## UI Management While I primarily manage Incus using the CLI, Incus has the ability to serve a UI that interacts with its API. This can be done by installing `incus-ui-canonical` provided by the Zabbly repository. ```bash terminal copy title="Install incus-ui-canonical" prompt="as user" sudo apt install -y incus-ui-canonical ``` By default Incus is not listening on a web port we can reach directly through the browser. We activate the Incus Web server by setting the `core.https_address` to port number 8443. An alternate port can be used if needed. ```bash terminal copy title="Enable Incus network availability" prompt="as user" incus config set core.https_address :8443 ``` After this, point your browser to https://127.0.0.1:8443 (https://127.0.0.1:8443) and follow the on screen instructions to login. If you are looking for more information about installing the UI checkout this article from Simos Xenitellis - How to install and setup the Incus Web UI (https://blog.simos.info/how-to-install-and-setup-the-incus-web-ui/) ## Example: Modeling a Distributed Web Stack Here’s a sample environment I run when I want to rehearse a distributed stack: - `app`: NGINX + PHP (Laravel, for example) - `db`: PostgreSQL - `cache`: ValKey/Redis - `proxy`: HAProxy handling SSL We can build this entire system inside Incus containers, each with its own IP on a private bridge network, and wire it up exactly the way we expect it to behave in production. I like to diagram the traffic flow on a whiteboard first; once we agree on the shape, the Incus commands come quickly, and we have a reproducible environment we can tear down or rebuild at will. ## Wrapping Up We just walked through the playbook I use to stand up a mini data center on a single machine with Incus, and hopefully you now have the same knobs to turn. From repositories and bridges to helper scripts and UI access, the workflow keeps us honest about how our distributed systems behave before they ever hit a real cloud. If you try this out, let me know what puzzles you run into—we can reason through the next iteration together, whether that’s adding observability tooling, expanding the cluster to another host, or automating instance creation with Terraform. ## Extra Resources * First steps with Incus (https://linuxcontainers.org/incus/docs/main/tutorial/first_steps/) * How to install and setup the Incus Web UI (https://blog.simos.info/how-to-install-and-setup-the-incus-web-ui/) * Prevent connectivity issues with Incus and Docker (https://linuxcontainers.org/incus/docs/main/howto/network_bridge_firewalld/#network-incus-docker) * How to configure your firewall (https://linuxcontainers.org/incus/docs/main/howto/network_bridge_firewalld/#network-bridge-firewall) ### i3wm, Twitter, and Cursing Developers URL: https://jasonjpeters.com/posts/i3wm-twitter-and-cursing-developers/ Description: Displaying @gitlost in i3wm - git commit -m '#!@*' Published: 2021-02-08 Tags: github, twitter, i3wm, shell Topics: github, social-media, linux They can be found in messages, images, film, and other, usually electronic mediums. Easter eggs coined by Steve Wright Director of Software Development at Atari Consumer Division during the the Atari 2600 Era when programmer Warren Robinett hid his initials in the seminal 1970 video game Adventure. Today the phenomenon can be found in pop culture including TV Shows like Fringe where small details (i.e. paint color splash) lead into the main plot point of the following episode, A Han Solo carbon figurine hidden by the cast in a scene of each episode of Jos Whedon's Firefly, or the plethora of details being discovered by fans within the MCU (Marvel Universe). Myself, and my colleagues have been known to also hide details of some joke or situation we are making fun of in our naming conventions for variables and files, print messages to the browsers console when the Konami code is entered, or in our commit messages. That brings us to the purpose of this article. Sometime ago my wife was listening to the Nerdcast from Brasil and brought my attention to a bot they mentioned that parses publicly available commit messages containing curses from GitHub and posts them anonymously to the Twitter account @gitlost. Read original article on dev.to (https://dev.to/jase/i3wm-twitter-and-cursing-developers-1ip8) ```bash terminal copy title="@gitlost" #!/bin/sh gitlost() { GITLOST=$(twurl "/1.1/statuses/user_timeline.json? screen_name=gitlost&include_rts=false&count=1");) GITLOST=$(echo "$GITLOST" | jq '.[] | .text' | sed 's/"//g') } i3status -c $HOME/.i3/i3status.conf | while : do gitlost read line echo "$GITLOST | $line" || exit 1 sleep 3600 done ``` ### 13thFloor_20151226 URL: https://jasonjpeters.com/posts/13thfloor-20151226/ Description: Poster design for 13thFloor Entertainment's December 26, 2015 event. Published: 2015-12-28 Tags: graphic art, poster Topics: graphic-art, poster-design A local archive entry for a 13thFloor Entertainment poster. ### 13thFloor_20151211 URL: https://jasonjpeters.com/posts/13thfloor-20151211/ Description: Poster design for 13thFloor Entertainment's December 11, 2015 event. Published: 2015-12-11 Tags: graphic art, poster Topics: graphic-art, poster-design A local archive entry for a 13thFloor Entertainment poster. ### 13thFloor_20151127 URL: https://jasonjpeters.com/posts/13thfloor-20151127/ Description: Poster design for 13thFloor Entertainment's November 27, 2015 event. Published: 2015-11-19 Tags: graphic art, poster Topics: graphic-art, poster-design A local archive entry for a 13thFloor Entertainment poster. ### Bassbong_Dubstep.v1 URL: https://jasonjpeters.com/posts/bassbong-dubstep-v1/ Description: Album cover artwork for a Bassbong Records dubstep release. Published: 2015-07-25 Tags: graphic art, album cover Topics: graphic-art, music-artwork A local archive entry for album cover artwork. ### Agustin URL: https://jasonjpeters.com/posts/agustin/ Description: Logo and type treatment for a British Columbia based DJ. Published: 2015-07-09 Tags: graphic art, logo Topics: graphic-art, logo-design A local archive entry for a DJ logo and type treatment. ### sub6 Audio Installation Logo URL: https://jasonjpeters.com/posts/sub6/ Description: Logo concept for sub6, a custom audio installation company, archived from a graphic design project. Published: 2015-03-09 Tags: graphic art, logo Topics: graphic-art, logo-design A local archive entry for an audio installation logo concept. ### Allday URL: https://jasonjpeters.com/posts/allday/ Description: Logo and typeset design for Allday, a British Columbia based DJ identity project. Published: 2015-02-06 Tags: graphic art, logo Topics: graphic-art, logo-design A local archive entry for a DJ logo and typeset piece. ### Meowface URL: https://jasonjpeters.com/posts/meowface/ Description: Logo and typeset design for Meowface, a British Columbia based DJ identity project. Published: 2014-07-31 Tags: graphic art, logo Topics: graphic-art, logo-design A local archive entry for a DJ logo and typeset piece. ### Dialate | Opened URL: https://jasonjpeters.com/posts/dialate-opened/ Description: CD cover artwork for Dialate | Opened, archived as a music release design project. Published: 2014-07-27 Tags: graphic art, cd cover Topics: graphic-art, music-artwork A local archive entry for CD cover artwork. ### 2010 CNC Design Show Poster URL: https://jasonjpeters.com/posts/2010-cnc-design-show-poster/ Description: Poster design for the 2010 College of New Caledonia Design Show. Published: 2014-04-25 Tags: graphic art, poster Topics: graphic-art, poster-design A local archive entry for a College of New Caledonia design show poster. ### 2011 CNC Design Show Poster Mock-Up URL: https://jasonjpeters.com/posts/2011-cnc-design-show-poster-mock-up/ Description: Poster mock-up for the 2011 College of New Caledonia Design Show. Published: 2014-04-25 Tags: graphic art, poster Topics: graphic-art, poster-design A local archive entry for a College of New Caledonia design show poster mock-up. ### bauhaus - kandinsky URL: https://jasonjpeters.com/posts/bauhaus-kandinsky/ Description: Bauhaus-inspired snowboard poster concept based on Kandinsky. Published: 2014-04-25 Tags: graphic art, poster Topics: graphic-art, poster-design A local archive entry for a Bauhaus-inspired snowboard poster concept. ### bauhaus - medeiros URL: https://jasonjpeters.com/posts/bauhaus-medeiros/ Description: Bauhaus-inspired snowboard poster concept for a College of New Caledonia course. Published: 2014-04-25 Tags: graphic art, poster Topics: graphic-art, poster-design A local archive entry for a Bauhaus-inspired snowboard poster concept. ### bauhaus - mondrian URL: https://jasonjpeters.com/posts/bauhaus-mondrian/ Description: Bauhaus-inspired snowboard poster concept based on Mondrian. Published: 2014-04-25 Tags: graphic art, poster Topics: graphic-art, poster-design A local archive entry for a Bauhaus-inspired snowboard poster concept. ### Bust It URL: https://jasonjpeters.com/posts/bust-it/ Description: Concept EDM flyer artwork exploring bold event-poster typography and electronic music visuals. Published: 2014-04-25 Tags: graphic art, flyer Topics: graphic-art, event-flyers A local archive entry for a concept EDM flyer. ### It's The Journey URL: https://jasonjpeters.com/posts/its-the-journey/ Description: Digital doodle artwork archived as a small experimental illustration and visual composition. Published: 2014-04-25 Tags: graphic art, digital art Topics: graphic-art, digital-art A local archive entry for a digital doodle. ### Pygmy URL: https://jasonjpeters.com/posts/pygmy/ Description: Illustration project for a College of New Caledonia course. Published: 2014-04-25 Tags: graphic art, illustration Topics: graphic-art, illustration A local archive entry for an illustration project. ### SBoard_TypeJP URL: https://jasonjpeters.com/posts/sboard-typejp/ Description: Hi-vis reflective and black print snowboard surface design. Published: 2014-04-25 Tags: graphic art, snowboard Topics: graphic-art, snowboard-design A local archive entry for snowboard surface design. ### SBoard_TypeS1 URL: https://jasonjpeters.com/posts/sboard-types1/ Description: Snowboard surface design concept using custom type, high-contrast layout, and board-scale graphics. Published: 2014-04-25 Tags: graphic art, snowboard Topics: graphic-art, snowboard-design A local archive entry for a snowboard design. ### We are connected URL: https://jasonjpeters.com/posts/we-are-connected/ Description: Billboard design promoting embracing differences in humanity. Published: 2014-04-25 Tags: graphic art, billboard Topics: graphic-art, billboard-design A local archive entry for a billboard design.