From Source to Production: How We Modernized Our App Delivery Pipeline
As part of the Neoground relaunch and the upcoming Charm v4 release, we redesigned how our web applications move from source control into production: isolated CI, reproducible build environments, verified artifacts and controlled deployment through Compass.

A deployment pipeline is easy to underestimate.
At first glance, deploying a web application appears to be a simple operation: retrieve the latest code, install dependencies, build assets, run a few application-specific commands and make the new version available.
That model can work remarkably well.
It can also accumulate responsibilities in places where they no longer belong.
As part of the complete Neoground website relaunch and the ongoing work toward Charm v4, we recently revisited how our applications move from source control into production.
The result is a new delivery model built around a simple principle:
Production should run prepared software, not manufacture it.
That principle led us to separate build, artifact handling and deployment into distinct layers, each with its own responsibilities and trust boundaries.
The first application to complete the entire pipeline was our new company website.
The previous model
Our existing deployment process was already automated.
A webhook could trigger an update on the target system, followed by the usual application build steps:
source repository
↓
deployment trigger
↓
production host
↓
Composer
↓
frontend dependencies
↓
asset build
↓
application-specific deployment tasks
For smaller systems, this approach is pragmatic and easy to operate.
The disadvantage is that the production host becomes both:
- the runtime environment;
- and the build environment.
It therefore needs access to package registries, frontend tooling, dependency resolvers and build-time dependencies that have little reason to exist there once the application is running.
The deployment also becomes partially dependent on the exact state of that server.
As our applications, infrastructure and internal tooling matured, that increasingly felt like the wrong boundary.
Moving the build away from production
The new model separates source, build and deployment:
source
↓
CI
↓
production artifact
↓
artifact management
↓
controlled deployment
A production artifact is no longer a source checkout waiting to be assembled.
It already contains the application in the state we intend to deploy.
For a typical Charm-based website, CI performs tasks such as:
- checking out the exact source revision;
- setting the intended production environment;
- installing Composer dependencies without development packages;
- performing an immutable frontend dependency install;
- compiling Sass and JavaScript assets;
- removing build-only dependencies;
- validating the resulting application tree;
- creating a compressed release artifact.
The target system no longer needs to perform that work.
CI requires its own trust boundary
One architectural requirement was clear early on: we did not want the CI runner living directly on an important application or infrastructure host.
CI systems execute repository-controlled instructions by design.
Once container tooling is involved, the privilege implications become particularly important. A workflow with broad control over the Docker environment can gain considerable authority over its host.
For that reason, our build environment is deliberately isolated from production infrastructure.
At our current scale, a dedicated virtual machine provides that boundary efficiently:
Forgejo
↓
dedicated runner environment
↓
Docker
↓
ephemeral build container
The implementation can evolve with workload.
A larger setup could move the same role to a dedicated appliance, virtualization platform or external compute instance without materially changing the application workflows.
The important property is not where the runner happens to execute.
The important property is that build workloads and production workloads do not share the same trust boundary.
A standardized Neoground build environment
The runner itself is not the application build environment.
For Charm-based applications, we created a reusable container image containing the common toolchain required by our current software stack.
That includes, among other components:
Debian 13
PHP 8.5
Composer
Node.js 24 LTS
Yarn 4
Valkey-compatible PHP tooling
Git
OpenSSH
rsync
tar
Zstandard
common PHP extensions
The image provides stable tooling.
The repository provides project-specific instructions.
This separation allows multiple applications to use the same build foundation without baking application source, configuration or credentials into the image.
A typical project pipeline can therefore remain comparatively small:
checkout
↓
composer install --no-dev
↓
yarn install --immutable
↓
build assets
↓
validate
↓
package
The exact build remains part of the project.
The environment in which it runs becomes standardized.
Reproducibility catches hidden assumptions
One of the advantages of moving builds into a clean environment is that assumptions which were previously implicit become visible.
During the first migration, an immutable Yarn install immediately detected that the project still carried state from an older Yarn generation.
The existing development environment could accommodate that history.
A clean CI environment correctly refused to rewrite the dependency state during a production build.
That is precisely the behavior we want.
A production pipeline should not silently decide that its dependency graph needs to change.
Instead, the repository now explicitly describes the expected package-manager state, and CI verifies it.
The same principle applies throughout the build:
If a release depends on something, that dependency should be visible and reproducible.
From repository to artifact
Once the production tree has been assembled, CI packages it as a compressed Zstandard archive.
A release consists conceptually of three objects:
application.tar.zst
application.tar.zst.sha256
application.json
The archive contains the prepared application.
The checksum verifies integrity.
The structured metadata describes the release.
That metadata can include information such as:
{
"project": "example",
"source": {
"commit": "...",
"ref": "main"
},
"artifact": {
"filename": "...",
"size_bytes": 13000000,
"sha256": "..."
},
"build": {
"environment": "Prod",
"php": "8.5.x",
"node": "v24.x",
"yarn": "4.x"
}
}
The source repository remains the authoritative history of the code.
The artifact describes what was actually built from that code.
That distinction becomes increasingly valuable once releases need to be inspected, compared, retained or redeployed.
CI does not deploy
Another deliberate boundary is that the CI pipeline stops after publication.
The build system is allowed to produce a release.
It does not decide that the release should become live.
Instead, CI publishes candidate artifacts into a tightly restricted ingress path.
The publishing credential is limited to that purpose and does not provide general administrative access to the target infrastructure.
Conceptually:
CI
↓
restricted artifact publication
↓
inbox
From there, our infrastructure layer takes over.
This limits the authority of the build system and keeps deployment decisions within the tooling responsible for managing infrastructure.
Where Compass takes over
We recently introduced Compass, our infrastructure management layer for operating the systems behind Neoground services and applications.
The new artifact workflow fits naturally into that architecture.
Once a release reaches the artifact ingress layer, Compass and the underlying provisioning system can handle the remaining lifecycle:
artifact inbox
↓
validation
↓
metadata ingestion
↓
release registration
↓
artifact storage
↓
deployment selection
↓
controlled rollout
Release metadata can be stored alongside the rest of the system state, allowing a specific application version to be selected later through the relevant administrative interface or command-line tooling.
This gives us a useful separation of concerns:
CI
Responsible for:
- source checkout;
- dependency resolution;
- compilation;
- testing and validation;
- artifact creation;
- integrity metadata;
- artifact publication.
Compass and provisioning
Responsible for:
- artifact ingestion;
- release inventory;
- deployment authorization;
- target selection;
- filesystem deployment;
- application lifecycle hooks.
This division keeps the delivery process understandable even as the surrounding systems grow.
Deployment still needs to understand the application
An artifact is only the master state of the application.
Deploying it correctly still requires understanding which parts of a running system are replaceable and which are persistent.
Charm applications may contain persistent application-specific data, logs and runtime cache structures that must survive a code deployment.
The deployment therefore does not blindly overwrite the live directory.
Instead, the provisioning layer works from a clean extracted release and reconciles that state with the target application while preserving the runtime data that belongs to the installation rather than the release.
Conceptually:
verified artifact
↓
clean staging tree
↓
reconcile with target
│
├── preserve persistent data
├── preserve logs
└── reset disposable cache state
↓
application post-deploy lifecycle
This is where the framework becomes part of the deployment contract.
Charm owns application-specific deployment work
Charm provides application-level tooling through Bob, including project-specific deployment commands.
Once the provisioning layer has installed the new application state, the application itself can perform the operations only it understands.
Depending on the project, this may include:
- database migrations;
- sitemap generation;
- application cache invalidation;
- generated metadata updates;
- project-specific maintenance operations.
The infrastructure system should not need to know why a particular application needs a sitemap rebuild.
It only needs to provide a reliable lifecycle point at which the application can perform that work.
This approach is also part of the ongoing modernization around Charm v4.
A framework is not only the code executed during an HTTP request. It also participates in development, testing, deployment and operations.
The new release pipeline gives us a cleaner interface between those layers.
The new Neoground website as the first full deployment
The first project to use the complete pipeline is the website you are reading now.
That makes the deployment work part of a larger milestone.
The site itself is a complete rebuild:
- a new repository;
- a new visual identity;
- a largely custom frontend rather than a generic Bootstrap foundation;
- a more structured component and interaction system;
- a modernized Charm application underneath;
- and now a new delivery pipeline around it.
A typical complete production build currently takes less than a minute, including dependency handling, frontend compilation, artifact creation, checksum generation, metadata generation and publication.
The resulting complete website artifact is compact enough that storage and transfer are operationally negligible.
That is the kind of result we prefer: a more disciplined system without introducing unnecessary platform complexity.
Modern does not have to mean complicated
There are much larger systems for solving software delivery.
For the right workload, they are entirely justified.
Our goal here was different.
We wanted explicit trust boundaries, reproducible builds, verifiable artifacts and controlled deployments without introducing infrastructure whose complexity exceeded the problem.
The resulting stack still relies on remarkably ordinary components:
Git
Forgejo
Linux
Docker
PHP
Node.js
Composer
Yarn
tar
Zstandard
SHA-256
SSH
rsync
The important engineering work lies in how those components are separated and how authority flows between them.
CI can build.
CI can publish.
CI cannot administer production.
Artifacts can be verified.
Infrastructure decides what gets deployed.
Applications remain responsible for their own post-deployment semantics.
That is a comparatively small system, but a much stronger contract.
Strategic technology, carried through to execution
Our current company positioning is:
Strategic technology, carried through to execution.
That principle applies internally as much as it does to client work.
Architecture matters.
Security boundaries matter.
Operational simplicity matters.
But ultimately the system still has to work.
In this case, the outcome is straightforward: a developer can push a change, a reproducible environment assembles the application, a verified release enters the infrastructure layer, and the selected version can be deployed through the same tooling that manages the rest of the system.
The website itself may be the visible result.
The engineering underneath it is what makes that result dependable.