App Deployment·8 min read·

Composer in Production Without Dev Dependencies

Install PHP packages for production with Composer --no-dev, a dumped autoloader, and a deploy user so PHPUnit and debugbars never reach the live VPS.

NB

Netbay Infrastructure Team

Netbay Engineering

On this page

Composer is how PHP applications declare their libraries. It is also how development tools leak into production. PHPUnit, Laravel Debugbar, Psalm, and faker do not belong on a public VPS. They waste disk, they waste opcache slots, and they occasionally expose routes or collectors that dump SQL. Production installs are a different command with a different autoloader dump, run as a deploy user, never as root, never as www-data with a writable vendor tree from the internet.

This post is the production Composer ritual on Ubuntu: install the binary correctly, pin composer.lock, install with --no-dev, dump an optimized autoloader, and keep vendor out of git. The same steps work for Laravel, Symfony, and a plain PHP app that uses composer.json.

Composer: dev machine versus production VPS Laptop / CI composer install require + require-dev PHPUnit, debugbar scripts: test, analyse composer.lock committed source of truth Production VPS composer install --no-dev require only -o autoloader dump no scripts that migrate vendor/ not in git lock file copied in Same lock file. Different install flags. Never composer update on the VPS.

Install Composer as a binary, not as a git clone

On Ubuntu, apt has a composer package, but it lags. The supported path is the official installer, then a copy into /usr/local/bin so every deploy user can run it. Verify the hash from getcomposer.org when you fetch the installer; do not curl-pipe-php from a random gist.

Keep Composer itself updated with composer self-update on a schedule, or pin a major version if your deploy is sensitive. PHP 8.3 on Ubuntu 24.04 wants Composer 2. Applications still on Composer 1 should be migrated before they hit this VPS.

Run Composer as the deploy user. Root installs create a root-owned vendor that FPM cannot read, or worse, a root-owned vendor that you then chmod 777 to make it work. The deploy user should own /var/www/app. www-data needs only execute on directories and read on files, plus write on storage paths.

Lock file is the artifact. composer.json is not enough.

composer.lock pins exact versions and hashes. Production must install from the lock. If the lock is missing, composer install resolves floating constraints and you no longer know what you shipped. Commit the lock for applications. For libraries the story differs; you are shipping an app.

Never run composer update on the VPS. update re-resolves constraints and rewrites the lock. That is a development action. Production is composer install with the lock you already reviewed in CI.

If CI builds vendor and you rsync the result, you still need the same flags. Building vendor in CI with --no-dev and uploading it is valid. Building vendor on the VPS with those flags is also valid. Mixing a laptop vendor directory that includes require-dev is how debugbar hits production.

The production install command

From the app root, as the deploy user, after git fetch and checkout of the release tag:

bash
cd /var/www/app
export COMPOSER_ALLOW_SUPERUSER=0
php -d memory_limit=512M /usr/local/bin/composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --classmap-authoritative --no-progress --no-ansi
php artisan config:cache
php artisan route:cache
php artisan view:cache

--no-dev skips require-dev. --prefer-dist pulls zipballs instead of git clones, which is faster and avoids shipping .git inside vendor. --optimize-autoloader dumps a classmap so the autoloader does not stat the filesystem on every class. --classmap-authoritative tells Composer that if a class is not in the classmap, it does not exist. That is a production-only flag. Do not use it while you are generating classes on the fly.

memory_limit=512M on the CLI invocation avoids FPM php.ini starving Composer. Composer is a CLI job. It should not run through php-fpm.

Skip --no-scripts only if your composer.json scripts are safe. Many Laravel apps run package:discover on install, which you want. Some apps run migrate on install, which you do not want coupled to vendor install. Read the scripts section. Prefer explicit artisan commands after install, as in the snippet above.

If you are not on Laravel, dump the autoloader the same way and skip artisan. A generic app still benefits from -o and --classmap-authoritative.

What not to ship

Do not copy node_modules, tests/, phpunit.xml, .env.example with secrets, or the local .env from a developer laptop. .env on the server is generated or copied from a secrets store, not from git. vendor/ is produced on the box or in CI, not committed.

Do not invent a flag that sometimes includes dev packages. Production has one path. Platform config belongs in composer.json so a laptop on PHP 8.4 cannot lock packages the VPS PHP 8.3 cannot run. CI should run composer install with the same PHP version as Lucknow production.

javascript
{
  "config": {
    "optimize-autoloader": true,
    "preferred-install": "dist",
    "sort-packages": true,
    "platform": {
      "php": "8.3.6"
    }
  },
  "require": {
    "php": "^8.3",
    "laravel/framework": "^11.0"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0",
    "laravel/pint": "^1.18"
  }
}

platform.php tells Composer to resolve as if it were 8.3.6 even when a developer runs 8.4. That prevents extension surprises on deploy. Keep require-dev populated for tests. Just never install it on the VPS.

Auth tokens and rate limits

Private Satis or GitHub tokens belong in auth.json under the Composer home directory, with mode 600, owned by the deploy user. Do not put tokens in composer.json. Do not print them in CI logs. Packagist public packages do not need a token; private VCS packages do.

On a small VPS, Composer can look heavy because it downloads zips onto High-Speed SSD and then unzips. That is fine. Do not point vendor at a network filesystem. Keep vendor next to the release so the autoloader classmap paths stay short and local.

After install, opcache will pick up new files only if validate_timestamps is on, or after an FPM reload. Production should reload php8.3-fpm at the end of the deploy. That is the same reload you run after php.ini changes.

Takeaway

Production Composer is install, not update; --no-dev, not default; optimized classmap, not PSR-4 scans on every request. Treat composer.lock as a build input, vendor as a build output, and require-dev as something that never crosses onto the VPS.

You can spin up an Ubuntu 24.04 instance on Netbay in under 60 seconds and run this install path on Intel Xeon Platinum - netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance