Use when creating and deploying a new Waaseyaa framework site to northcloud.one. Covers app scaffold, Caddy config, Deployer, GitHub Actions, server setup, and common pitfalls.
End-to-end guide for creating and deploying a new site built on the Waaseyaa PHP CMS framework to production on northcloud.one.
| Item | Value |
|---|---|
| Server | <server-hostname> (<server-ip>) |
| Web server | Caddy (NOT Nginx) |
| PHP | 8.4, FPM socket at /run/php/php8.4-fpm.sock |
| Deploy user | <deploy-user> (has NOPASSWD sudo for caddy and php-fpm reload/restart, and Caddyfile writes) |
| Admin user | <admin-user> (NOPASSWD sudo ALL) |
| Caddy runs as |
caddy user, primary group caddy. Supplementary groups from /etc/group are NOT available under systemd. |
| Framework repo | waaseyaa/framework (public, no PAT needed) |
| Framework branch for CI | develop/v1.1 — verify packages exist on this branch before adding deps |
cd ~/dev && gh repo create waaseyaa/<site-name> --public --description "<description>" --clone
cd ~/dev/<site-name>
/vendor/
/.build/
/.env
*.sqlite
storage/framework/packages.php
The Waaseyaa kernel requires the full package stack to boot. There is no lite mode. Use @dev stability (not @alpha) for local path repos.
{
"name": "waaseyaa/<site-name>",
"description": "<description>",
"type": "project",
"license": "MIT",
"require": {
"php": ">=8.4",
"waaseyaa/access": "^0.1@dev",
"waaseyaa/admin-surface": "^0.1@dev",
"waaseyaa/ai-agent": "^0.1@dev",
"waaseyaa/ai-pipeline": "^0.1@dev",
"waaseyaa/ai-schema": "^0.1@dev",
"waaseyaa/ai-vector": "^0.1@dev",
"waaseyaa/api": "^0.1@dev",
"waaseyaa/cache": "^0.1@dev",
"waaseyaa/cli": "^0.1@dev",
"waaseyaa/config": "^0.1@dev",
"waaseyaa/database-legacy": "^0.1@dev",
"waaseyaa/entity": "^0.1@dev",
"waaseyaa/entity-storage": "^0.1@dev",
"waaseyaa/field": "^0.1@dev",
"waaseyaa/foundation": "^0.1@dev",
"waaseyaa/graphql": "^0.1@dev",
"waaseyaa/i18n": "^0.1@dev",
"waaseyaa/mcp": "^0.1@dev",
"waaseyaa/media": "^0.1@dev",
"waaseyaa/menu": "^0.1@dev",
"waaseyaa/node": "^0.1@dev",
"waaseyaa/note": "^0.1@dev",
"waaseyaa/path": "^0.1@dev",
"waaseyaa/plugin": "^0.1@dev",
"waaseyaa/queue": "^0.1@dev",
"waaseyaa/routing": "^0.1@dev",
"waaseyaa/search": "^0.1@dev",
"waaseyaa/ssr": "^0.1@dev",
"waaseyaa/state": "^0.1@dev",
"waaseyaa/taxonomy": "^0.1@dev",
"waaseyaa/typed-data": "^0.1@dev",
"waaseyaa/user": "^0.1@dev",
"waaseyaa/validation": "^0.1@dev",
"waaseyaa/workflows": "^0.1@dev"
},
"repositories": [
{"type": "path", "url": "../waaseyaa/packages/*"}
],
"autoload": {
"psr-4": {
"SiteName\\": "src/"
}
},
"extra": {
"waaseyaa": {
"providers": [
"SiteName\\Provider\\SiteServiceProvider"
]
}
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}
Do NOT add Waaseyaa\Note\NoteServiceProvider to providers — it is auto-discovered from the note package's own composer.json. Adding it manually causes a "type already registered" error.
Before adding a package, verify it exists on the develop/v1.1 branch:
git ls-tree --name-only origin/develop/v1.1 packages/ | sort
If a package only exists on main, use "dev-main" as the version constraint instead of "^0.1@dev".
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
$kernel = new Waaseyaa\Foundation\Kernel\HttpKernel(dirname(__DIR__));
$kernel->handle();
Minimal config for a static/marketing site:
<?php
declare(strict_types=1);
return [
'database' => null,
'ssr' => [
'theme' => '',
'cache_max_age' => 300,
],
];
mkdir -p storage/framework src/Controller src/Provider templates public/css
composer install
<?php
declare(strict_types=1);
namespace SiteName\Provider;
use Waaseyaa\Foundation\ServiceProvider\ServiceProvider;
use Waaseyaa\Routing\RouteBuilder;
use Waaseyaa\Routing\WaaseyaaRouter;
final class SiteServiceProvider extends ServiceProvider
{
public function register(): void {}
public function routes(WaaseyaaRouter $router): void
{
$router->addRoute(
'page.home',
RouteBuilder::create('/')
->controller('SiteName\\Controller\\PageController::home')
->render()
->methods('GET')
->build(),
);
}
}
<?php
declare(strict_types=1);
namespace SiteName\Controller;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Twig\Environment;
use Waaseyaa\SSR\SsrResponse;
final class PageController
{
public function __construct(
private readonly Environment $twig,
) {}
public function home(array $params, array $query, $account, HttpRequest $request): SsrResponse
{
return new SsrResponse($this->twig->render('home.html.twig', ['path' => '/']));
}
}
rm -f storage/framework/packages.php
REQUEST_METHOD=GET REQUEST_URI=/ SERVER_NAME=localhost php public/index.php
Should output HTML. If it outputs JSON:API errors, check the "Common Pitfalls" section.
Create Caddyfile in the project root (NOT in ops/nginx/):
# <domain> - included via import in main /etc/caddy/Caddyfile
# Deploy path: ~/<project>, current release: ~/<project>/current
<domain> {
tls {
issuer acme {
}
}
root * /home/<deploy-user>/<project>/current/public
encode gzip zstd
@css {
path /css/*
}
handle @css {
header Cache-Control "public, max-age=31536000, immutable"
file_server
}
@js {
path /js/*
}
handle @js {
header Cache-Control "public, max-age=31536000, immutable"
file_server
}
@images {
path /img/*
}
handle @images {
header Cache-Control "public, max-age=31536000, immutable"
file_server
}
@ico {
path *.ico
}
handle @ico {
header Cache-Control "public, max-age=31536000, immutable"
file_server
}
@robots {
path /robots.txt
}
handle @robots {
header Cache-Control "public, max-age=3600"
file_server
}
file_server
php_fastcgi * unix//run/php/php8.4-fpm.sock {
resolve_root_symlink
}
log {
output file /home/<deploy-user>/<project>/log/access.log {
mode 0644
}
}
}
<?php
namespace Deployer;
require 'recipe/common.php';
set('application', '<project>');
set('keep_releases', 5);
set('allow_anonymous_stats', false);
set('shared_dirs', ['storage']);
set('shared_files', ['.env']);
set('writable_dirs', ['storage', 'storage/framework']);
host('production')
->setHostname('<domain>')
->set('remote_user', 'deployer')
->set('deploy_path', '/home/<deploy-user>/<project>')
->set('labels', ['stage' => 'production']);
desc('Upload pre-built release artifact from CI');
task('deploy:upload', function (): void {
upload('.build/', '{{release_path}}/', [
'options' => ['--recursive', '--compress'],
]);
});
desc('Clear Waaseyaa framework manifest cache');
task('waaseyaa:clear-manifest', function (): void {
run('rm -f {{release_path}}/storage/framework/packages.php');
});
desc('Reload PHP-FPM to pick up new release');
task('php-fpm:reload', function (): void {
run('sudo systemctl reload php8.4-fpm');
});
desc('Deploy to production');
task('deploy', [
'deploy:info',
'deploy:setup',
'deploy:lock',
'deploy:release',
'deploy:upload',
'deploy:shared',
'deploy:writable',
'waaseyaa:clear-manifest',
'deploy:symlink',
'deploy:unlock',
'deploy:cleanup',
'php-fpm:reload',
]);
after('deploy:failed', 'deploy:unlock');
If the app has database migrations, add a migrate task before deploy:symlink:
task('waaseyaa:migrate', function (): void {
run('set -a && . {{deploy_path}}/shared/.env && set +a && php {{release_path}}/bin/migrate');
});
Create .github/workflows/deploy.yml: