A lightweight, bash-based server provisioning and deployment system for Laravel projects. A self-hosted alternative to Laravel Forge, targeting Ubuntu 24.04.
- Full server provisioning: NGINX, PHP 8.1–8.4, MariaDB, Redis, Node.js, Supervisor, Certbot
- Site management: One command to create a fully configured Laravel site (FPM pool, vhost, database, queue workers, scheduler, SSL)
- Zero-downtime deployments: Atomic symlink swap with automatic rollback support
- CI/CD ready: GitHub Actions workflows for automatic deployment on push
- Idempotent: Every script is safe to re-run
- CLI tools: Unified
forge-litecommand plusforge-lite-db,forge-lite-ssl,forge-lite-env,php-switch
- Fresh Ubuntu 24.04 server
- Root access (or sudo)
- SSH key-based access configured
Provision a fresh server without logging in — paste cloud-init/cloud-config.yml as User Data when creating the server. Edit the configuration variables at the top of the runcmd block (repo URL, PHP version, passwords, etc.) before use.
Works with Hetzner Cloud, DigitalOcean, AWS EC2, Vultr, Linode, and any provider that supports cloud-init.
Monitor progress on the server:
tail -f /var/log/forge-lite-cloud-init.loggit clone https://github.com/Padrio/Forge-Lite.git /opt/forge-lite
cd /opt/forge-lite/clisudo ./forge-lite provisionOptions:
--php-default=8.3 Default PHP CLI version
--db-password=PASS MariaDB root password (auto-generated if omitted)
--redis-password=PASS Redis password (auto-generated if omitted)
--node-version=22 Node.js major version
--skip-reboot Don't reboot after provisioning
--force Re-provision (ignores existing marker)
sudo forge-lite site add \
--domain=example.com \
--php=8.3 \
--queue-workers=2 \
--sslOptions:
--domain=DOMAIN Domain name (required)
--php=VERSION PHP version (default: 8.3)
--queue-workers=N Queue worker processes (default: 2)
--enable-ssr Enable Inertia SSR process
--enable-horizon Use Horizon instead of queue workers
--no-scheduler Disable Laravel scheduler
--ssl Issue SSL certificate
--env=KEY=VALUE Extra .env variable (repeatable)
Configure repo-based deployments (one-time):
sudo forge-lite deploy setup example.com \
--repo=git@github.com:your-org/your-app.git \
--branch=mainDeploy (uses saved repo config):
sudo forge-lite deploy example.comManual deployment with explicit repo:
sudo forge-lite deploy example.com \
--repo=git@github.com:your-org/your-app.git \
--branch=mainArtifact deployment (from CI):
sudo forge-lite deploy example.com \
--artifact=/tmp/deploy-artifact.tar.gzOptions:
--repo=URL Git repository URL
--branch=BRANCH Git branch (default: main)
--artifact=PATH Deploy from a tar.gz artifact instead of git
--keep=N Number of releases to keep (default: 5)
Rollback:
sudo forge-lite rollback example.comDeployments use self-hosted runners on your server — no SSH secrets needed. Multiple runners can run on the same server (one per repo/environment).
Set up a runner (per repo):
-
Go to your GitHub repo → Settings → Actions → Runners → New self-hosted runner
-
Copy the registration token
-
On your server:
sudo forge-lite runner setup \ --repo=git@github.com:your-org/your-app.git \ --token=AXXXXXXXXXXXXXXXXXXXXXXXXXXXX \ --labels=forge-lite,production
The runner name is auto-derived from the repo (
your-app), or set explicitly with--name=myapp. Both SSH and HTTPS repo URLs are supported. -
Verify the runner is online:
sudo forge-lite runner status sudo forge-lite runner list
Multiple runners on one server:
# Production app
sudo forge-lite runner setup \
--repo=git@github.com:org/app.git \
--token=TOKEN1 \
--name=app-production \
--labels=forge-lite,production
# Staging app
sudo forge-lite runner setup \
--repo=git@github.com:org/app.git \
--token=TOKEN2 \
--name=app-staging \
--labels=forge-lite,staging
# Different repo
sudo forge-lite runner setup \
--repo=git@github.com:org/other-app.git \
--token=TOKEN3Remove a runner:
sudo forge-lite runner remove --name=app-staging --token=REMOVAL_TOKENConfigure workflows:
- Edit
templates/workflows/deploy-production.yml— set your domain - Edit
templates/workflows/deploy-staging.yml— set your staging domain - Push to
main(production) ordevelop(staging) to trigger deployment
Multi-server setup: Use different labels per server (e.g., forge-lite,production vs forge-lite,staging) and match them in the workflow files.
After provisioning, these are installed to /usr/local/bin/:
sudo php-switch 8.4 # Switch default PHP CLI + FPMsudo forge-lite-db create myapp # Create database + user
sudo forge-lite-db list # List databases
sudo forge-lite-db backup myapp # Backup to /home/deployer/backups/
sudo forge-lite-db restore myapp dump.sql.gz
sudo forge-lite-db drop myapp --yes # Drop database + usersudo forge-lite-ssl issue example.com # Obtain certificate
sudo forge-lite-ssl renew example.com # Force renew
sudo forge-lite-ssl status example.com # Show certificate infosudo forge-lite-env list example.com # Show all .env variables
sudo forge-lite-env get example.com APP_KEY # Get a variable
sudo forge-lite-env set example.com KEY VALUE # Set a variable
sudo forge-lite-env delete example.com KEY # Remove a variablesudo forge-lite update # Reinstall CLI tools and bash completionssudo forge-lite runner setup --repo=URL --token=TOKEN [--name=NAME] [--labels=LABELS]
sudo forge-lite runner remove --name=NAME --token=TOKEN
sudo forge-lite runner status
sudo forge-lite runner listList all sites:
sudo forge-lite site listRemove a site:
sudo forge-lite site remove example.com
sudo forge-lite site remove example.com --keep-db --keep-files/home/deployer/sites/<domain>/
├── releases/ # Timestamped release directories
│ ├── 20240101_120000/
│ └── 20240102_150000/
├── shared/
│ ├── .env # Shared environment file
│ └── storage/ # Shared Laravel storage
└── current -> releases/XXX # Symlink to active release
/etc/forge-lite/<domain>.conf # Site configuration (KEY=VALUE)
/root/.forge-lite-credentials # Generated passwords (chmod 600)
| Component | Details |
|---|---|
| System | deployer user, UTC timezone, en_US.UTF-8 locale, open file limits |
| Swap | Size based on RAM, sysctl tuning, OOM priorities |
| Security | SSH hardening, UFW (22/80/443), Fail2Ban, unattended-upgrades |
| NGINX | Mainline, DH params, gzip, rate limiting, security headers |
| PHP | 8.1, 8.2, 8.3, 8.4 with FPM + Laravel extensions + OPcache/JIT |
| Composer | Global install with weekly auto-update |
| MariaDB | Secured, InnoDB tuned (70% RAM), forge-lite admin user |
| Redis | Password-protected, AOF, maxmemory (25% RAM), allkeys-lru |
| Node.js | v22 via NodeSource |
| Supervisor | For queue workers, Horizon, SSR |
| Certbot | Let's Encrypt with nginx plugin + auto-renewal |
| GitHub Runner | Self-hosted Actions runner (optional, via forge-lite runner setup) |
View credentials:
sudo cat /root/.forge-lite-credentialsCheck site config:
cat /etc/forge-lite/example.com.confTest NGINX config:
sudo nginx -tView FPM status:
sudo systemctl status php8.3-fpmView queue worker logs:
tail -f /home/deployer/sites/example.com/shared/storage/logs/worker.logRe-provision (safe — idempotent):
sudo forge-lite provision --force --skip-rebootMIT