← back to home
root@souverainete:~$ cat etude-de-cas/grav.md
case study — stack: grav cms

A reusable base to deploy and maintain multiple Grav sites

Rather than automating the installation of a single site, an architecture separating runtime, applications, persistent data and deployment mechanism — designed to control the lifecycle of multiple Grav sites without duplicating their operations.

Type of projectReusable deployment base
Starting pointInternal documentation site running Grav
Initial constraintPreserve content across redeployments
StackLinux · Ansible · Docker · Grav · Git
// context

The problem started with persistence

The first need was fairly simple: automate the deployment of a Grav site used as internal documentation.

But this site had to keep evolving after deployment. Pages would be edited from the admin interface, images added, accounts created.

Redeploying the application could therefore never reset the site to its initial state and destroy data produced in operation.

A first boundary appeared:

The application must be replaceable. User data must survive.

Persistent content was therefore separated from the container's lifecycle.

Then other projects needed Grav sites. The question was no longer: How do I deploy this site? but:

How do I have a mechanism to deploy and maintain multiple Grav sites without duplicating the operational logic?

That's the moment a deployment problem became an architecture problem.

// stakes

What had to be reconciled

Persistence

Content created in operation must survive a container being replaced, a redeployment, and a version change.

Reusability

The shared mechanics must not be copied and adapted for every new project.

Maintainability

Runtime, application, data and deployment don't evolve at the same pace or for the same reasons. Their separation should reflect these lifecycles.

Reversibility

Rolling back to a previous version should use, as much as possible, the same mechanism as a normal deployment.

Security

Secrets and sensitive configuration stay out of application images and versioned code.

// architecture

Different lifecycles, separated responsibilities

Analyzing the first deployment, then the arrival of new projects, brought out four distinct domains: runtime, application / site, persistent data and deployment mechanism. Grouping them together just because they serve the same site would have created unnecessary coupling. The architecture therefore aims to make these boundaries explicit.

// grav-runtime (shared base) → independent applications → ansible-role-grav-site (shared deployment) → instances → data decoupled from the container
A shared runtime

grav-runtime provides the shared technical foundation: Grav, PHP, Nginx and the contract expected by applications. It knows nothing about any particular business project.

Independent applications

projet-gites, projet-lavallee-website and future sites add only what's specific to them: theme, plugins, application configuration and initial content. Each application stays independently versioned.

A shared deployment mechanism

ansible-role-grav-site centralizes the shared mechanics: preparing the instance, persistent volumes, configuration, secrets, startup, health checks, updates and rollback. The role stays focused on one instance per invocation.

Data outside the container's lifecycle

Pages, images, accounts and other persistent data live independently of the application image. A new version can therefore replace the application without automatically resetting its production state.

// implementation

From installation to lifecycle

01

First need: making data persistent

The first deployment made it possible to formalize a fundamental rule: rebuildable elements belong to the application; data produced in operation must live outside the container.

02

Extracting a shared runtime

Grav, PHP, Nginx and the shared dependencies were isolated into grav-runtime. Application projects no longer have to rebuild this technical layer.

03

Centralizing deployment

The operational mechanics were grouped into ansible-role-grav-site: volumes, secrets, configuration, health checks, deployed version, update and rollback.

04

Moving to multiple applications

projet-gites, then lavallee.tech, made it possible to test the model against several real applications. The second site in particular exposed assumptions still tied to the first project — names, paths or instance properties — and triggered their generalization.

// lifecycle

Installing is only the first step

An application is only installed once. It then has to be operable for years. The model being sought is therefore no longer:

install

but:

deploy → update → rollback → migrate

This difference shapes the architecture. The runtime is versioned. Each application is versioned. Data persists independently. The deployment mechanism applies the desired state.

// rollback

Rollback stays a deployment

Rollback isn't designed as a separate emergency procedure. If a new version causes a problem, a previously known-stable version simply becomes the desired version again.

version 1.3.2
      │
      ▼
 deploy 1.4.0
      │
    issue
      │
      ▼
 target = 1.3.2
      │
      ▼
same deployment mechanism

This keeps special procedures confined to exactly the moment they'd be riskiest: during an incident.

// result

What this architecture changes

Deployment logic is centralized instead of being copied into every project. Each site keeps its own content, configuration and versioning cycle — and replacing an application never means replacing its persistent state.

1shared runtime, built and maintained independently of the sites
1deployment mechanism, instead of being copied per project
2independent applications deployed on the same base

The first site solved a need. The second started testing the architecture. The next ones must be able to reuse the same base without multiplying the operational logic.

// proof

The second site is the real test

It's easy to call an architecture "reusable" while it still serves only one project. The second consumer reveals the assumptions that were actually specific to the first.

lavallee.tech is now itself deployed on this architecture. Its arrival made it possible to identify the last instance parameters that needed generalizing for the deployment role to stay an atomic, reusable component.

The next step is to use this same mechanism for a new technical documentation site. Reusability is therefore not treated as a declared property.

It has to be verified by new consumers.

New technical documentation site in progress

The same base — grav-runtime + ansible-role-grav-site — will be reused without modification for this new consumer, the only real proof that an architecture is reusable.

// sovereignty

Why this architecture also supports control of the system

Using open source software is only part of the problem. Infrastructure remains hard to control if its deployment depends on manual steps, if its data is mixed in with the application, or if nobody knows precisely which version is in production.

This architecture therefore aims to make explicit:

  • the components in use;
  • the versions deployed;
  • the data to preserve;
  • the responsibilities of each layer;
  • the mechanism to rebuild an instance.
Open source is a means. Control of the system is the goal.
// limits

What the base isn't trying to do

Content creation and editorial design stay specific to each application.

The deployment role doesn't manage DNS, reverse proxy, or TLS certificates. Those responsibilities belong to the shared infrastructure.

Backups are still necessary. Grav avoids running an extra database service, but the persistent files are production data and must be backed up.

The deployment role manages one instance per invocation. Orchestrating multiple sites is deliberately handled at a higher level.

Applications requiring complex business features, heavy traffic, or advanced data models may fall outside the natural scope of a flat-file CMS like Grav.

// components

The base and its applications

grav-runtime
Shared runtime. Versioned base image providing Grav, PHP, Nginx and the shared runtime contract.
View on GitHub →
ansible-role-grav-site
Deployment mechanism. Ansible role responsible for the lifecycle of a compatible Grav instance.
View on GitHub →
projet-gites
Application. First business site using the shared base.
View on GitHub →
projet-lavallee-website
Application. Second real site using the same architecture — the one you're currently looking at.
View on GitHub →
// go further

Why this architecture?

This separation wasn't born from a theoretical design. It results from the evolution of a concrete need: preserving the data of a first site, then understanding how to maintain multiple applications without duplicating their infrastructure.

Read the article: From Deploying One Website to Designing a Reusable Deployment Platform →

A Linux service to make reproducible and maintainable?

Automated deployment is only part of the problem. The real question is how the system will be updated, diagnosed, backed up, restored, and handed off over time.

Let's talk about your constraints first →