Installation and operation of the Manage server. Requirements: PHP 8.0 or newer and a web server. No database server, no Composer, no build step.
Put the repository into a directory of the web server, for example
/var/www/manage.
Create the configuration:
cp config.sample.php config.php
Generate a password hash and enter it:
php -r 'echo password_hash("a-long-password", PASSWORD_DEFAULT), PHP_EOL;'
define("MANAGE_ADMIN_PASSWORD_HASH", '$2y$12$…');
Use single quotes — a bcrypt hash contains $.
Set the public URL. Without this value, no client can download a package:
define("MANAGE_PUBLIC_URL", "https://manage.example.org");
Absolute, without a trailing slash. If the installation lives in a
subdirectory, that belongs too: https://example.org/manage.
Name the product. MANAGE_PACKAGE_PREFIX determines the filename of
stored packages and should match the project's build script:
define("MANAGE_PRODUCT_NAME", "Example Orderform");
define("MANAGE_PACKAGE_PREFIX", "example-orderform");
Make sure storage/ is writable. The directory is created on demand if
needed:
mkdir -p storage && chown www-data:www-data storage && chmod 2775 storage
Open the UI: https://manage.example.org/admin/login.php.
Under Settings → Diagnostics you can then see whether everything essential checks out: public URL, write access, password, upload limits.
The bundled .htaccess locks storage/, includes/, client-package/ and
config.php, and sets security headers. It only works when
AllowOverride All is set for the directory.
.htaccess has no effect under nginx. The locks must be set by hand:
location ^~ /storage/ { deny all; return 404; }
location ^~ /includes/ { deny all; return 404; }
location ^~ /client-package/ { deny all; return 404; }
location = /config.php { deny all; return 404; }
location ~ /\. { deny all; return 404; }
Verify the locks are effective:
curl -s -o /dev/null -w "%{http_code}\n" https://manage.example.org/config.php
curl -s -o /dev/null -w "%{http_code}\n" https://manage.example.org/storage/instances.json
Both must return 403 or 404.
Backups and release packages are uploaded over HTTP. Both PHP limits need to
be large enough, with post_max_size at least as large as
upload_max_filesize:
upload_max_filesize = 256M
post_max_size = 256M
max_execution_time = 300
memory_limit = 256M
The current values are shown on the diagnostics page. If a value is too small, the client reports an error message that names the cause explicitly.
memory_limit becomes relevant when the S3 archive is active: an upload to
S3 holds the file in memory in full.
Configurable under Settings, stored in storage/settings.json. The
values there take precedence over the constants in config.php.
Changes apply immediately, not only on the next upload.
Without S3, all backups sit on the local disk. With S3, every received backup is additionally pushed to an S3-compatible object storage system; locally, only the newest copies remain.
define("MANAGE_S3_ENABLED", true);
define("MANAGE_S3_ENDPOINT", "https://fsn1.your-objectstorage.com");
define("MANAGE_S3_REGION", "fsn1");
define("MANAGE_S3_BUCKET", "my-backup-bucket");
define("MANAGE_S3_PREFIX", "manage-backups");
define("MANAGE_S3_ACCESS_KEY", "…");
define("MANAGE_S3_SECRET_KEY", "…");
Objects live under <prefix>/<instance>/<filename>.
Addressing: the default is virtual-hosted
(https://<bucket>.<endpoint>/<key>), which Hetzner and most providers
expect. If the provider requires path-style, set MANAGE_S3_PATH_STYLE to
true.
Behavior:
Troubleshooting via storage/logs/s3.log and the excerpt under
Settings: AccessDenied or a redirect in the status chain almost always
points to the wrong addressing style; SignatureDoesNotMatch to a wrong
region or secret key. Redirects are deliberately not followed, so a
misconfiguration stays visible.
The Manage server holds release packages and every instance's backups — it is itself worth backing up. To back up:
storage/ – instances, manifest, packages, backups, settingsconfig.php – credentialsWith the S3 archive active, backups additionally live in the bucket; but
storage/instances.json and storage/releases/ do not.
storage/logs/ and rotate automatically.config.php; logged-in sessions remain
valid until they expire. To end them immediately, clear
session.save_path or change session_name in includes/auth.php.