Browse the manual
Install with Docker or a PaaS
Run one persistent Made with Pepper installation with Docker Compose. Keep data and dashboard updates through container replacement, and assess PaaS storage requirements.
On this page
The downloaded ZIP includes the Docker files. First installation takes three Docker commands, then browser setup. After that, start the existing installation with PEPPER_HTTPS=on docker compose up -d app.
You need Docker with the Compose plugin, shell access and an HTTPS address for the app. The recipe includes PHP, Apache and the scheduler. Choose SQLite during setup if you want to run without a separate database server. You need no Composer or Node.js on the server.
Install
- Download the ZIP from your purchase or support channel. Check its supplied SHA-256, then extract it into a new folder. Keep this copy intact.
- Open a terminal in its
resources/dockerfolder and run:
docker compose build --pull app
docker compose --profile setup run --rm initialize
PEPPER_HTTPS=on docker compose up -d app
- Connect your HTTPS domain to the app's private port,
127.0.0.1:8100. Use your host's reverse proxy, or the Caddy example below. Openhttps://your-domain/installonce the proxy serves HTTPS. - Read the setup token from the container and enter it in the browser:
docker compose exec --user www-data app \
cat storage/app/private/installation/setup-token
- Follow browser setup: choose SQLite or your supported MySQL/MariaDB server, enter your business details, create the owner account and configure email. Sign in. Open Settings > Backups and check that the scheduler reports a recent run. Add no second scheduler or queue worker.
Run the commands from the same resources/docker folder each time. The Compose project name is pepper; its volume is pepper_installation. The initialization command copies the package once, into an empty volume. It refuses existing or incomplete contents instead of replacing them.
The volume holds your complete installation: code, settings, key, files, database when using SQLite, and update recovery files. Container replacement keeps it. Do not delete the volume or run docker compose down -v when updating. Keep one app instance and an off-server full backup.
For a separate installation, use docker compose -p another-name in every command. Keep each existing installation's project name unchanged.
HTTPS
The app port stays private. For Caddy installed on the same server, point your domain's DNS to the server and use:
pepper.example.com {
reverse_proxy 127.0.0.1:8100
}
Allow Caddy's public HTTP/HTTPS ports through your firewall. Caddy's guide covers installation and certificates. For a proxy inside Docker, join the app's network and send traffic to app:80.
PEPPER_HTTPS=on tells PHP that your proxy serves HTTPS. Use it only with an HTTPS public route. Keep it in each docker compose up command, or in your platform's environment settings. The container retains that setting through automatic restarts.
The installer saves the URL it sees. Use your final HTTPS address for setup. Host-loopback requests reach PHP through Docker's network, so plain HTTP setup does not count as loopback access. You can check container health at /up.
MySQL certificate settings, when needed
The runtime's MySQL/MariaDB backup client verifies the database certificate. A successful PHP database connection alone does not prove that backups work. Use the provider's exact TLS hostname. If its certificate needs a private CA, obtain the public CA certificate from the database provider and keep it outside the package seed. Do not disable certificate verification to fix a backup error.
For example, save that CA as /srv/pepper-db-ca.pem. Create /srv/pepper-db-client.cnf without credentials:
[client]
ssl-ca=/etc/pepper-db-ca.pem
Create /srv/pepper-db-tls.yaml:
services:
app:
environment:
MYSQL_ATTR_SSL_CA: /etc/pepper-db-ca.pem
volumes:
- /srv/pepper-db-ca.pem:/etc/pepper-db-ca.pem:ro
- /srv/pepper-db-client.cnf:/etc/mysql/conf.d/pepper-tls.cnf:ro
Run Compose with both files: docker compose -f compose.yaml -f /srv/pepper-db-tls.yaml. Use that command in place of docker compose for later starts and runtime rebuilds. Keep these mounts in every future deployment. The environment setting configures PHP and the scheduler. The runtime passes the public CA path to scheduled jobs. The client file configures database dumps and restores. Test a backup and restoration on staging before production. A hostname error needs a server certificate for the hostname you use, even when the CA is correct.
Update the application
- Create and download a full export from Settings > Backups. Keep its recovery key outside the server.
- Obtain the new intact buyer ZIP and test it on a separate staging installation.
- Follow the dashboard update guide: upload the ZIP, click Check package, then Install update. Keep the page open for the result.
- Check the installed version, a retained record and an uploaded file. Confirm that scheduled work resumes.
The updater replaces application code on the persistent volume. It keeps your environment, key, records and files. These code changes survive container replacement. Do not rebuild the runtime or run the initializer to install a new application version.
If the dashboard upload is too large, first open Settings > Update to create its private folder. Then copy the intact ZIP from the host:
docker compose cp /srv/downloads/new-release.zip app:/tmp/pepper-update.zip
docker compose exec --user root app chown www-data:www-data /tmp/pepper-update.zip
docker compose exec --user www-data app \
mv /tmp/pepper-update.zip storage/app/private/updates/new-release.zip
Use your actual ZIP path and a new destination name. Return to Settings > Update, click Refresh list and check the package. Do not extract the ZIP over the application.
If an update fails, Made with Pepper restores the previous files and database. If recovery cannot finish, it stays in maintenance mode. Preserve the volume and read update recovery and operator recovery commands. Do not delete or initialize the volume to get past a failure.
Update PHP and the container runtime
PHP security patches and operating-system updates need a runtime rebuild. Application updates need the dashboard procedure above. For a runtime rebuild:
- Take an off-server full export and test the new runtime on staging.
- Keep the same Compose project and installation volume. Keep a tag for the previous runtime image so you can return to it if needed.
- Rebuild and recreate:
docker image tag pepper-runtime:local pepper-runtime:previous
docker compose build --pull app
PEPPER_HTTPS=on docker compose up -d --force-recreate app
docker compose logs --tail=50 app
- Check sign-in, a retained record, files, installed version and scheduler status.
If this runtime cannot start the existing installation, stop it, tag pepper-runtime:previous as pepper-runtime:local, then run PEPPER_HTTPS=on docker compose up -d --force-recreate --no-build app. Keep the existing volume. This returns to the previous PHP image; it does not undo an application update or restore a database.
Retain the recipe and project configuration used for this installation. A later recipe may require its own migration instructions. Monitor free disk space and rotate storage/logs/scheduler.log; the scheduler appends its output there.
PaaS options
The Compose deployment is the tested recipe. The platform list below describes storage capabilities checked in the providers' documentation on 11 October 2026. These are candidates or known limits, not completed Made with Pepper deployment certifications. Provider features and plans can change.
A platform must let you:
- Run this PHP/Apache image on container port 80, with an always-running scheduler.
- Attach one writable persistent filesystem to
/var/www/pepper, including code and recovery files. SQLite needs a local disk with normal locking; do not use object-storage mounts or an unverified shared network filesystem. - Initialize that empty volume from the intact package at runtime. The volume must be available to the initialization command; a build or pre-deploy hook often has no volume.
- Keep one instance, prevent overlapping deployments and retain the same disk through redeployments.
- Use a shell or file transfer for the package, possession token and recovery. Keep HTTPS at the public proxy and set
PEPPER_HTTPS=onafter you enforce it.
| Platform | Position for this recipe | Operator requirement |
|---|---|---|
| Docker Compose on a Linux server or VPS | Tested deployment model | Keep a named local volume and the same project name. Host choice is independent of the recipe; Docker's installation guide lists supported operating systems. |
| Coolify, self-hosted or Coolify Cloud connected to your server | Candidate with Compose and named volumes | Keep the volume mount on one server. Run initialization against that exact volume; adapt the proxy route and package path. |
| Dokploy, self-hosted or Dokploy Cloud | Candidate with Docker Compose | Use Compose mode, a named volume and one instance. Arrange the package upload and initialization before normal startup. |
| CapRover | Candidate with persistent apps; needs adapted deployment configuration | Mark the app persistent and mount /var/www/pepper. Keep one instance on its storage server. Provide an initialization command and package transfer. |
| Railway | Candidate with a volume; needs adapted deployment configuration | Mount at /var/www/pepper and keep one running instance. Volumes exist at runtime, not build or pre-deploy time. Arrange runtime initialization and test persistence before production. |
| Render | Candidate on a paid service with a disk; needs adapted deployment configuration | Use a Docker web service and persistent disk at /var/www/pepper. Keep one instance. Initialize while the disk is mounted; expect downtime during replacement. Free ephemeral services do not meet the storage requirement. |
| Fly.io | Candidate with a Machine and local volume; needs adapted deployment configuration | Mount a Fly Volume at /var/www/pepper. Keep one Machine, keep it running and retain its volume. Separate Machines have separate volumes. |
| Heroku dynos | Does not meet this recipe's persistence requirement | Files on a dyno's ephemeral filesystem disappear at restart. An external database alone does not preserve the installed code, key or files. |
| DigitalOcean App Platform | Does not meet this recipe's persistence requirement | App Platform does not support persistent volumes. A separate Linux server can use the Compose model above. |
| AWS App Runner | Does not meet this recipe's persistence requirement | App Runner supports stateless applications with ephemeral storage. Use a separately operated server/container platform with a persistent local disk instead. |
| Google Cloud Run | Needs a different deployment design | Cloud Run targets services that need no local persistent filesystem. Network or object-storage mounts do not establish support for this updater and SQLite recipe. |
First setup on a managed platform
On a server you control, use the Compose steps above and keep its volume name. A managed platform needs another way to transfer the package and run initialization against its mounted disk. Use this procedure only when its shell/file-transfer tools and startup settings permit these steps:
- Build this runtime from
resources/docker, or configure the provider to build that directory. If it needs a registry image, push your runtime image to your own registry and select it in the provider. The runtime image contains no buyer application or installation data. Keep the buyer ZIP private. - Create one service with one persistent disk at
/var/www/pepper. Set the public proxy's target port to 80, prevent overlapping replicas and keep the service running between requests. UsePEPPER_HTTPS=onafter the proxy enforces HTTPS. - For this first, empty installation, set a temporary startup command. It serves a plain setup notice on port 80 so you can connect with the provider's runtime shell. It starts no Made with Pepper application or scheduler:
sh -c 'mkdir -p /seed /tmp/pepper-bootstrap; \
printf "Waiting for package initialization.\n" > /tmp/pepper-bootstrap/index.html; \
exec php -S 0.0.0.0:80 -t /tmp/pepper-bootstrap'
- Keep this temporary service private to the operator. Transfer the extracted, intact package contents, including
.env.exampleand empty storage directories, into/seedthrough the provider's shell or file-transfer tools. Confirm/seed/artisanand/seed/release-manifest.jsonexist. Keep this seed outside the installation disk; upload no live.env, database, logs or private files. - Run
pepper-initializeas root in the same container. It checks/seedand initializes/var/www/pepper. A refusal needs investigation; it never authorizes overwriting the disk. - Replace the temporary startup command with
pepper-startand redeploy with the same disk. Remove the temporary package-transfer access. Open the HTTPS domain and complete possession-gated browser setup above.
Do not use the temporary command for an existing installation. Do not put initialization in a build hook, an unmounted pre-deploy hook or every normal restart. If the platform cannot provide runtime shell/file transfer and an attached disk during these steps, use a Compose server instead. Platform image/registry and shell commands differ; the provider's own documentation owns those account operations.
For a candidate platform, first deploy a separate trial with synthetic data. Install, create a record and upload a file, take a full export, update, and redeploy the container. Confirm that the key, record, file and updated version stay intact, and that the scheduler runs. Test recovery before moving production data. Use the full import guide for an existing business; stop the source during a move and import into an empty destination. Do not point fresh setup at a populated business database.
Initialization or startup refused
| Message or symptom | Action |
|---|---|
| Volume is not empty | Confirm the project and volume names. If it is your existing installation, start it without initialization. Preserve unknown contents for investigation. |
| Missing, extra or changed package file | Obtain an intact buyer ZIP and extract it into another empty directory. Keep deployment settings outside that seed directory. |
.pepper-initializing remains after a failed copy |
Preserve the failed volume. For a fresh trial, choose a new empty volume and initialize it from the intact package. Never retry over partial data or remove this guard from an existing installation. |
| Installation absent or incomplete | Check that the intended initialized volume mounts at /var/www/pepper. Do not create a replacement volume over your existing data. |
| Requirements fail or update check fails | Read the stated PHP, extension, database, permission, disk-space or backup-client cause. Fix it on staging, then check again. |
| No scheduler report | Keep one container running and inspect storage/logs/scheduler.log. Confirm the platform gives it CPU between web requests. |
Need help with the product?
Contact support