How to make Laravel Ridiculously Fast on a 5$/month VPS

Light Speed!

In the first post, I focused on constraints and philosophy. This follow-up walks through the actual implementation, how I made Markdown files behave like database records, built custom recipe syntax, automated responsive images, and why the most boring tech stack turned out to be the most fun.

NOTES

  • critical css
  • no js
  • laravel caching
  • no db
  • server setup (opcache, fastcgi)
  • Cloudflare setup (cache rules)

Optimize your dependencies

CSS and JS can balloon up in size and slow down your site if left unchecked.

Cache everything

If your app has a lot of expensive queries, make use of Laravel's various caching mechanism and use a fast cache driver like Redis

Do you really need a Database?

Maybe you don't need a DB model for everything, consider using Sushi for models with mostly static data

Server Setup

The "Database" That Isn't

The most interesting architectural choice was ditching the database entirely for content. Articles and recipes are just .md files with front matter, but they still behave like Eloquent models thanks to Sushi.

class Article extends Model
{
    use Sushi;

    public function getRows()
    {
        return Collection::make(
            Finder::create()->in(resource_path('views/content'))->files()
        )
        ->filter(fn($file) => Str::endsWith($file->getFilename(), '.md'))
        ->transform(function (SplFileInfo $file) {
            $object = YamlFrontMatter::parseFile($file->getRealPath());
            // Parse filename like: 2025-08-29-how-i-rebuilt-this-site.md
            preg_match('/^(\d{4}-\d{2}-\d{2})-(.+)/',
                $file->getFilenameWithoutExtension(), $matches);

            return [
                'title' => $object->matter('title'),
                'slug' => $matches[2] ?? $filename,
                'date' => $matches[1] ?? null,
                'summary' => $object->matter('summary'),
                'body' => $object->body(),
            ];
        })->values()->toArray();
    }
}

This is perfectly simple. I get all the benefits of Eloquent ( relationships, scopes, pagination) without any of the database overhead. Articles are versioned in Git and editable in any text editor.

Custom Markdown Extensions: The Fun Part

The real engineering happened in two custom CommonMark extensions. These let me author content naturally while getting structured output.

Responsive Images

The ResponsiveImageExtension intercepts every ![](image.jpg) in Markdown and generates responsive variants automatically:

public function render($node, ChildNodeRendererInterface $childRenderer): string
{
    $responsiveData = $this->imageService->generateResponsiveImages($url);

    return '<picture>'.
        '<source type="image/webp" srcset="'.$data['webp_srcset'].'" sizes="'.$data['sizes'].'">'.
        '<img src="'.$data['original'].'" srcset="'.$data['srcset'].'" alt="'.$alt.'" loading="lazy">'.
        '</picture>';
}

I just write ![Alt tag](image.png) in Markdown and get automatic responsive images with modern formats. Zero build pipeline, zero external services.

Cooklang: Structured Recipes in Plain Text

For recipes, I implemented Cooklang. It hits the sweet spot, structured enough to be useful, simple enough to write by hand.

Add @flour{200g} and @sugar{50g} to a bowl.
Mix with #whisk for ~timer{5 minutes}.

The CooklangExtension includes custom inline parsers for each syntax:

// Matches @ingredient{quantity}
class IngredientInlineParser extends AbstractInlineParser
{
    public function getMatchDefinition(): InlineParserMatch
    {
        return InlineParserMatch::regex('@([^{]+)(?:\{([^}]*)\})?');
    }

    public function parse(InlineParserContext $inlineContext): bool
    {
        $ingredient = $cursor->getMatches()[1];
        $quantity = $cursor->getMatches()[2] ?? '';

        $inlineContext->getContainer()->appendChild(
            new IngredientNode($ingredient, $quantity)
        );

        return true;
    }
}

Each parser creates custom AST nodes (IngredientNode, CookwareNode, TimerNode) that get rendered as semantic HTML with CSS classes for styling. The extension also processes the final document to extract ingredient lists automatically.

The Markdown Configuration

I'm using Spatie's laravel-markdown package as the foundation, then layering on custom extensions. The result renders with caching enabled, so even complex recipes with lots of images stay fast.

Performance: The Boring Wins

  • Server-rendered HTML: No client-side framework overhead
  • Cached Markdown rendering: Parse once, serve many times
  • Responsive images with WebP: Automatic modern formats
  • Minimal JavaScript: Just progressive enhancements where needed
  • Tailwind Typography: Clean styles without reinventing typography

The whole site feels instant because there's barely any JavaScript to load or execute. Sometimes the best performance optimization is not doing the work at all.

What I Didn't Build

  • No admin interface: Files and Git are my CMS
  • No API: Just server-rendered HTML
  • No database: Sushi handles everything in memory
  • No build pipeline: Images process on-demand
  • No search: Simple navigation is enough for now
  • No comments: Maybe later, maybe never

Each missing feature represents complexity I didn't have to manage. That's time I can spend writing instead of debugging deployment pipelines.

The Experience of Building This

Building with constraints turned out to be liberating. Every decision defaulted to "simpler" instead of "more features." The custom Markdown extensions were the only genuinely complex part, and even those are just a few hundred lines total.

This technical stack is exactly boring enough. Laravel handles routing and templating. Sushi makes files behave like models. Custom Markdown extensions add just enough structure without breaking the writing flow.

The result is a site that feels hand-crafted but doesn't require constant maintenance. No dependency updates breaking the build. No JavaScript framework migrations. No database migrations or backup strategies.

Just words, images, and enough code to make them look good on the web.

If you're building your own site, consider boring tech. Pick the stack you already know. Add custom touches only where they matter. Ship something small and perfect instead of something large and complicated.