Deployment
By the end of this lesson you'll be able to take a PHP app from your laptop to a live, hardened server — wiring up Nginx or Apache with PHP-FPM, tuning php.ini for production, installing dependencies the right way, keeping secrets out of git, and shipping new releases with zero downtime.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ The Web Server + PHP-FPM
A web server like Nginx does not run PHP itself. It serves static files (CSS, JS, images) directly and forwards every .php request to PHP-FPM — the "PHP FastCGI Process Manager", a separate program that keeps a pool of worker processes ready to execute your code. The two talk over a Unix socket (a fast local pipe). The golden rule: point the web server at your public/ folder so only index.php is reachable, and the rest of your code stays private.
Apache is just as common (it ships with cPanel hosting and XAMPP). It connects to the same PHP-FPM socket using mod_proxy_fcgi , so the PHP side is identical — only the web-server syntax differs.
2️⃣ Hardening php.ini for Production
Your local php.ini is tuned for debugging — it prints errors to the screen and re-checks every file on each request. In production you want the opposite. Two settings matter most: display_errors = Off so a crash never prints file paths or secrets onto the page a visitor sees (you log them privately instead), and opcache.enable = 1 so PHP compiles each file once and reuses the result — the single biggest speed win you get for free.
One gotcha with opcache.validate_timestamps = 0 : because OPcache no longer re-reads files, it won't see your new code until you reload PHP-FPM on every deploy . That single systemctl reload is what makes a release "go live" — forget it and the server keeps serving the old code.
3️⃣ Installing Dependencies the Production Way
On the server you never run a plain composer install . You add two flags. --no-dev skips development-only packages (PHPUnit, debug toolbars) that should never reach production. --optimize-autoloader builds a single class-map lookup table instead of scanning folders on every request, so your app boots faster. Add --classmap-authoritative to trust that map fully and skip filesystem checks.
4️⃣ Environment Variables & Secrets
Database passwords and API keys must never live in your PHP source or in git — anyone who clones the repo would get them. Instead they go in a .env file that exists only on the server and is listed in .gitignore . Your code reads them at runtime with getenv() (or a framework helper), so the same code runs everywhere and only the values change between machines.
5️⃣ Zero-Downtime Releases
Copying new files over a running site is risky — for a few seconds visitors hit a half-updated app. The fix is the symlink release pattern. Each deploy lands in its own timestamped folder, and a symlink called current points at the live one. Because switching a symlink with ln -sfn is atomic (it happens in one instant), the site flips from old to new with zero gap — and rolling back is just pointing the symlink at the previous folder.
Now you try. The php.ini below is almost production-ready — fill in each ___ using the 👉 hint, reload PHP-FPM, and check it against the Output panel.
6️⃣ CI/CD & Docker Overview
You don't want to deploy by hand. A CI/CD pipeline (Continuous Integration / Continuous Deployment) runs on every push: it installs dependencies, runs your tests, and only deploys if they pass. Docker takes it further by packaging your exact PHP version, extensions, and code into one image that runs identically everywhere. A multi-stage build installs Composer deps in a throwaway stage and copies only the result into a slim final image — and that image runs as www-data , never root.
One more. Write the production install command CI should run — add the two flags from the 👉 hints so dev tools are skipped and the autoloader is optimised.
📋 Quick Reference — Deployment
No code is filled in this time — just a brief and an outline. Write the zero-downtime deploy script yourself, run it against a test VPS (or a folder on your machine to dry-run the symlink logic), then check it against the expected output in the comments. This is the exact write-run-check loop you'll use to ship real releases.
Practice quiz
What is PHP-FPM's job in a production stack?
- It serves static CSS and images
- It compiles JavaScript
- It actually runs your PHP code; the web server forwards .php requests to it
- It stores session cookies
Answer: It actually runs your PHP code; the web server forwards .php requests to it. Nginx/Apache don't execute PHP — they forward each .php request to PHP-FPM, which keeps a pool of worker processes ready.
Where should the web server's document root point?
- The public/ folder, so only index.php is reachable
- The project root, exposing all files
- The vendor/ folder
- The home directory
Answer: The public/ folder, so only index.php is reachable. Point at public/ so only index.php is web-reachable and the rest of your code stays private.
Why must display_errors be Off in production?
- Errors slow the server down
- It saves disk space
- It is required to enable OPcache
- A crash would print file paths and secrets straight into the visitor's page
Answer: A crash would print file paths and secrets straight into the visitor's page. Set display_errors = Off and log_errors = On so errors go to a private log, not the page — keep error_reporting = E_ALL to still record everything.
What is the single biggest free speed win you enable in production php.ini?
- memory_limit = 1024M
- opcache.enable = 1
- expose_php = On
- display_errors = On
Answer: opcache.enable = 1. OPcache compiles each PHP file once and reuses the bytecode — typically a 2-3x speed-up with no code changes.
What does 'composer install --no-dev --optimize-autoloader' do?
- Skips dev-only packages and builds a fast class-map autoloader
- Installs PHPUnit and debug tools
- Deletes the vendor folder
- Upgrades every package to the latest version
Answer: Skips dev-only packages and builds a fast class-map autoloader. --no-dev skips test/debug tools that shouldn't ship; --optimize-autoloader builds a single class-map lookup so the app boots faster.
Where do database passwords and API keys belong?
- Hard-coded in your PHP source
- Committed to git so the team can share them
- In a .env file on the server, listed in .gitignore
- In the public/ folder
Answer: In a .env file on the server, listed in .gitignore. Secrets live in a server-only .env (never in code or git); PHP reads them at runtime with getenv(). Commit a .env.example with empty values.
Why is switching a symlink with 'ln -sfn' good for zero-downtime deploys?
- It copies files faster than cp
- It is atomic — the site flips from old to new in one instant with no half-deployed gap
- It compresses the release
- It restarts the database
Answer: It is atomic — the site flips from old to new in one instant with no half-deployed gap. Each release lands in its own timestamped folder and 'current' points at the live one; the atomic symlink swap means visitors never hit a partial deploy.
With opcache.validate_timestamps=0, what must you do after every deploy?
- Reboot the whole server
- Run composer update
- Clear the browser cache
- Reload PHP-FPM so it loads the new code
Answer: Reload PHP-FPM so it loads the new code. OPcache no longer re-reads files, so 'systemctl reload php8.3-fpm' is what makes a release go live; forget it and the old code keeps serving.
What does a CI/CD pipeline do on each push?
- Automatically writes new features
- Installs dependencies, runs tests, and only deploys if they pass
- Deletes old branches
- Emails the whole company
Answer: Installs dependencies, runs tests, and only deploys if they pass. Tests gate the deploy, so broken code never ships. Docker takes it further by packaging one identical image.
How should a production Docker container run PHP-FPM?
- As root for full permissions
- With display_errors On
- As an unprivileged user (www-data), never as root
- Without OPcache
Answer: As an unprivileged user (www-data), never as root. A multi-stage build produces a slim image that runs as www-data — running as root is a security risk.