SERVER_SETUP.md 5.8 KB

Setting Up the Server

Overview

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.

Installation

  1. Put the repository into a directory of the web server, for example /var/www/manage.

  2. Create the configuration:

    cp config.sample.php config.php
    
  3. 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 $.

  1. 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.

  1. 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");
    
  2. 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
    
  3. 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.

Web server

Apache

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.

nginx

.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.

Upload limits

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.

Retention

Configurable under Settings, stored in storage/settings.json. The values there take precedence over the constants in config.php.

  • Local backups per instance – default 30, minimum 1
  • S3 backups per instance – default 365, only with the S3 archive active

Changes apply immediately, not only on the next upload.

S3 archive (optional)

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:

  • S3 errors never fail a client upload.
  • A local copy is only deleted once it has fallen out of local retention and the S3 copy is confirmed.
  • Failed uploads are retried on the next upload from the same instance, or via "Catch up S3 uploads now".
  • The bucket can and should stay private: downloads go through the UI.
  • When enabling this on an existing installation, press "Catch up S3 uploads now" once so the archive catches up.

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.

Backing up the server

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, settings
  • config.php – credentials

With the S3 archive active, backups additionally live in the bucket; but storage/instances.json and storage/releases/ do not.

Operation

  • Login is rate-limited per IP (default 10 attempts per 15 minutes).
  • Rate limiting also applies to failed API authentications.
  • Logs live under storage/logs/ and rotate automatically.
  • A password change happens in 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.

Next