---
title: "Building @avelor/vhost: From Repeated Pain to Open Source Tool"
date: 2026-06-02
type: systems
description: "@avelor/vhost generates Apache VirtualHost configs from a YAML file — with a managed-config marker for gradual migration, a default-vhost that blocks unrecognized requests, and content-negotiated error pages that serve HTML or JSON based on the Accept header."
tags: [apache, vhost, cli, developer-tools, open-source]
project: avelor
canonical: https://octa.page/doc/building-avelor-vhost/
---

Every new domain starts the same way. Copy a VirtualHost template, replace three strings, uncomment the SSL block after Certbot runs, verify the HTTP→HTTPS redirect is there. Repeat for aliases. The template is maybe 40 lines. After the tenth domain, you have ten variants, some with subtle differences: a missing `AllowOverride All`, a wrong log path, an alias that didn't make it to the `RewriteCond`.

The other half of the problem is invisible. Apache's default behavior when a VirtualHost doesn't match is to fall through to the first `.conf` alphabetically. On a server with ten domains, an unrecognized request — a crawler, a scanner, a misconfigured DNS — silently gets served content from whichever site happens to sort first. No error page. No indication anything went wrong.

The fix for both problems is the same: stop treating VirtualHost configs as files you edit and start treating them as artifacts you generate.

## The tool

`@avelor/vhost` is a Node.js CLI that takes a `sites.yml` and produces Apache configs.

```yaml
sites:
  avelor.com:
    aliases: [www.avelor.com]
    mode: static
    root: /home/avelor/avelor

  api.avelor.es:
    mode: proxy
    port: 3000
```

`sudo vhost apply` generates the `.conf` files, runs `a2ensite`, runs `apache2ctl configtest`, and reloads Apache. Every generated file has `# Generated by @avelor/vhost` on the first line. That comment is load-bearing — `vhost check` reads it to distinguish generated configs from hand-written ones.

## Gradual migration

The tool was designed around an existing server with hand-written configs. Requiring a full migration upfront is a non-starter: ten domains, some with complex rewrite rules, some with custom headers. One bad config and the whole server is down.

The managed marker solves this without ceremony. Every `.conf` that predates `vhost apply` shows up in `vhost check` as `manual config` — functional, not yet under source-of-truth control:

```
  ✓  avelor.com        managed   /home/avelor/avelor
  !  old-client.com    manual config   not in sites.yml
  !  staging.app.com   manual config   → run: vhost apply staging.app.com to migrate
```

Migration is one site at a time. Add the domain to `sites.yml`, run `vhost apply <domain>`, the generated config overwrites the manual one. When `vhost check` shows no `manual config` entries, the migration is complete.

## Error pages via content negotiation

`vhost apply` installs built-in error pages for the full 4xx/5xx range. The design constraint: a server that hosts both web apps and APIs needs error responses in the right format depending on who's asking.

The implementation uses Apache's `MultiViews` content negotiation with static files. Each error code has three variants:

```
/var/www/vhost-errors/
  404.html   404.json   404.xml
  500.html   500.json   500.xml
  ...
```

The `ErrorDocument` directive points to the path without extension:

```apache
ErrorDocument 404 /vhost-errors/404
```

Apache picks `.html`, `.json`, or `.xml` based on the request's `Accept` header. No PHP, no CGI, no runtime. The JSON response is `{"code": 404, "message": "Page not found."}`. The HTML page shows the status code large, a generic message, and `Avelor · vhost` fixed at the bottom.

## Why open source

Avelor already publishes independent developer tools. The pattern is consistent: if the problem is universal and the solution has no coupling to private infrastructure, it goes public.

The Apache VirtualHost problem isn't specific to Avelor. Any developer running multi-domain Apache on a VPS encounters it. The tool is a YAML-to-Apache-config transformer with a CLI and nothing else. Keeping it private would be the stranger choice.