LAMP Stack on Alibaba Cloud ECS: From Fresh Instance to Production-Ready Web Server
Set up a LAMP stack (Linux, Apache, MySQL, PHP) on Alibaba Cloud ECS. Covers security groups, service installation, Discuz deployment, source compilation, hardening and three-tier scale-out.
You have a fresh ECS instance and SSH access. Your goal is a public website running Apache, PHP and MySQL. Between you and that goal sit three classes of problems that catch every beginner the first time:
- Network reachability – packets are silently dropped at the cloud security group, the OS firewall, or the listening socket, and the symptom is the same in all three cases: nothing happens.
- Service wiring – Apache, PHP and MySQL are three separate processes that have to find each other through file extensions, Unix sockets and TCP ports. Each interface has its own failure mode.
- Identity and permissions – Apache runs as
www-data, MySQL runs asmysql, files are owned byrootafterwget. The wrong combination produces 403, “Access denied”, orchmod 777desperation.
This guide walks through all of them in the order you actually hit them on day one, then keeps going into the things that show up on day thirty: TLS, virtual hosts, backups, source compilation, and when to stop running everything on a single box.
What you will be able to do after reading
- Build a mental model of how an HTTP request travels through Linux, Apache, PHP and MySQL, and predict where it will break.
- Configure Aliyun networking from the security group inwards, with a real defence-in-depth model rather than
0.0.0.0/0everywhere. - Install, verify and harden each LAMP component on Ubuntu (the steps for CentOS / Alibaba Cloud Linux are called out alongside).
- Deploy a non-trivial application end-to-end (Discuz!), including the file-permission and database-account work that the docs gloss over.
- Diagnose the five failures that account for ~90% of “my LAMP doesn’t work” tickets.
- Decide when to stay on a single ECS, and when to split into SLB + ECS + RDS.
Prerequisites
- An Alibaba Cloud ECS instance, Ubuntu 22.04 LTS or Alibaba Cloud Linux 3 / CentOS 7+.
- SSH access from your laptop using a key (not a password).
- Comfort with the Linux command line:
ls,cd,cat,systemctl,sudo. - A domain name is optional but nice to have for the TLS section.
1. Why LAMP still earns its place
LAMP – Linux + Apache + MySQL + PHP – has been declared dead in every web framework cycle since 2010 and refuses to oblige. The reason is not nostalgia, it is fitness for purpose. For content sites, CMS platforms (WordPress, Discuz, Drupal, MediaWiki), customer portals, internal tools and a long tail of small SaaS backends, LAMP is the most cost-effective, best-documented and lowest-maintenance way to put dynamic web pages in front of users.
What you actually get for free with LAMP that newer stacks make you reassemble:
- A mature ecosystem. Apache is twenty-eight years old, MySQL twenty-eight, PHP thirty. Almost every problem you can have has been hit, written up and indexed.
- Shared-hosting parity. A LAMP app moves between a $5/month shared host and an ECS without a code change.
- A predictable request path. No service mesh, no sidecar, no orchestrator – one process tree on one machine. When latency rises you can
topyour way to the answer. - A low operational floor. One server, three services. The cognitive load is a fraction of Kubernetes.
LAMP is not the right answer when your workload is high-fanout APIs (use Nginx + Go/Node/Rust), event-driven and connection-heavy (websockets at scale prefer event loops over Apache prefork), or when your team has already invested in containers and a control plane. Pick LAMP for what it is good at, not as a default.
2. The four-layer architecture

The diagram above is the single most important picture in this guide. Internalise it and most operational problems become “which layer is broken” instead of “the server doesn’t work”.
Each layer owns something specific:
- Linux owns processes, files and sockets. If
systemctl status apache2saysinactive, the rest does not matter. - Apache owns the HTTP wire format and the mapping from URL to handler. If Apache is not loaded, port 80 is just a closed socket; if it is loaded but no
VirtualHostmatches yourHost:header, you fall through to the default page. - PHP owns code execution. Apache hands it a
.phpfile; PHP parses it, runs it, returns text. If PHP is missing or its module is not enabled, Apache happily serves your source code as plain text – a security incident dressed as a misconfiguration. - MySQL owns durability. If MySQL is down, PHP scripts that need data raise exceptions; if MySQL is up but the credentials are wrong, the same scripts produce blank pages.
The interfaces between the layers are the parts that fail:
| Interface | What can go wrong | Fast check |
|---|---|---|
| Linux -> Apache | service not started, port 80 in use | ss -tlnp | grep ':80' |
| Apache -> PHP | php module not enabled | apache2ctl -M | grep php |
| PHP -> MySQL | extension missing, wrong host/socket | php -r "var_dump(extension_loaded('mysqli'));" |
| MySQL -> disk | data directory permissions, full disk | journalctl -u mysql -n 50 |
Memorise these four checks and you will resolve most LAMP issues without ever opening Stack Overflow.
3. Anatomy of an Aliyun ECS instance

Before installing anything, pick the right instance. The console shows hundreds of options; in practice for a starter LAMP server you decide on four things:
Region and zone. A region is a city (Hangzhou, Beijing, Singapore); a zone is a data centre inside that city. Latency to your users is set by the region; resilience to one DC failure is set by the zone. For a single-instance LAMP you only pick one zone – there is no point pretending to be multi-AZ on one machine.
Instance family. The naming is <family><generation>.<size>. For a public LAMP site:
g7.large(2 vCPU / 8 GiB) is the safe default – balanced compute and memory.c7.largeif your workload is mostly PHP (CPU-bound) and your DB is small.r7.largeif your workload is read-heavy and you can win by caching aggressively in MySQL’s buffer pool.t6burstable instances cost a fraction ofg7and are fine for a low-traffic blog – as long as you understand CPU credits run out.
Disk. Choose ESSD PL1 over basic cloud disks. The IOPS difference (5000 vs ~1000) is the difference between a snappy admin panel and a slow one, and the price gap on small disks is small. Forty GiB is enough for the OS plus a moderate site – attach a separate data disk if your database will grow past a few gigabytes.
Public IP. You can take the public IP that comes with the instance (cheap, but bound to the instance and lost on release) or attach an Elastic IP (EIP) which survives instance changes. For anything you might rebuild, pay the small EIP fee.
That is the entire decision. Skip the long list of features and confirm.
4. Networking: the part that traps everyone
Every “I can’t reach my server” question on the Aliyun forum has the same root cause – packets are dropped at one of the four points in the path:
client laptop ---internet---> [security group] ---> [OS firewall] ---> [listen socket] ---> Apache
You have to open all four, or you will diagnose the wrong layer.
4.1 Public IP
In the ECS console, Instances -> your instance -> Networking -> Bind EIP (or assign a public IP at create time). Note the address; treat it like a domain name (8.134.207.88 is the example used below).
4.2 Security group rules
The security group is a stateful packet filter that lives in the cloud, not on the OS. It runs before anything reaches your instance, so it overrides whatever your OS firewall says. In the console: Security Groups -> Configure Rules -> Inbound.
A safe starter rule set for a public LAMP server:
| Protocol | Port range | Source | Purpose | Notes |
|---|---|---|---|---|
| TCP | 22/22 | your home IP/32 | SSH | Never 0.0.0.0/0. Use curl ifconfig.me to find your IP. |
| TCP | 80/80 | 0.0.0.0/0 | HTTP | Only as a 301 -> https redirect. |
| TCP | 443/443 | 0.0.0.0/0 | HTTPS | The only port the public actually talks to. |
| TCP | 3306/3306 | (closed) | MySQL | Never open. Reach DB via SSH tunnel. |
| ICMP | -1/-1 | 0.0.0.0/0 | Ping | Optional, useful for monitoring. |
If you genuinely need remote MySQL access for a developer, add their IP only:
| |
Compared to opening 3306 on the security group, the tunnel:
- reuses your existing SSH key auth (no extra credential),
- only exposes the DB while the tunnel is up,
- never appears in shodan scans.
4.3 OS-level firewall
The cloud security group is necessary but not sufficient – a future operator might open everything on the security group “to debug”, and your second line of defence is the OS firewall.
On Ubuntu / Debian:
| |
On CentOS / Alibaba Cloud Linux (firewalld):
| |
4.4 Verifying reachability hop by hop
When something does not respond, isolate the failing hop in this exact order. Doing it out of order is how you spend three hours debugging the wrong thing.
| |
5. The request flow, end to end

When the request finally lands and Apache answers, this is what happens. Knowing this flow turns “the site is broken” into a sequence of yes/no questions.
- Browser opens a TCP connection to
8.134.207.88:80. - Aliyun’s security group accepts the SYN (rule for tcp:80 from 0.0.0.0/0).
- The kernel hands the connection to whoever is listening –
apache2. - Apache parses the request line
GET /index.php HTTP/1.1, walks itsVirtualHostconfig to find one whoseServerNamematches theHost:header, then resolves/index.phpagainst that vhost’sDocumentRoot. - The
mod_phphandler matches.phpand Apache invokes the embedded PHP interpreter (or, with FPM, opens the unix socket and forwards the request). - The PHP script runs; one of its first statements is usually
new mysqli('localhost', ...)ornew PDO('mysql:host=localhost;...'). PHP opens a TCP connection to127.0.0.1:3306(or, on Debian/Ubuntu, the unix socket/var/run/mysqld/mysqld.sock). - MySQL authenticates the user, parses the SQL, hits the InnoDB buffer pool (or disk if cold), returns rows.
- PHP renders the rows into HTML, returns it to Apache, which writes it on the wire.
The classic failure modes are annotated under the figure. The most common are:
- PHP shows as plain text. Apache is serving the file but is not invoking PHP. The handler module is not loaded.
- Blank page after install. PHP errors are being suppressed and the script crashed – look in
/var/log/apache2/error.log, not in the browser. - “Connection refused” intermittently. The MySQL connection limit is hit, or the OOM killer just shot
mysqld. Checkdmesgandmysql.err.
6. Installing the stack on Ubuntu
Step zero before installing anything: make sure no other web server or database is already on the box.
| |
The order matters: install Apache, then MySQL, then PHP last. PHP’s package will pull in the Apache module and will run a post-install hook that enables it – this only works if Apache is already there.
6.1 Apache
| |
Visit http://YOUR_PUBLIC_IP/ – you should see the Apache2 Ubuntu Default Page. If you do not, run the four-step verification from section 4.4 in order.
The directories you will actually edit:
| Path | What lives there |
|---|---|
/etc/apache2/apache2.conf | global config – almost never edit directly |
/etc/apache2/sites-available/*.conf | virtual host definitions |
/etc/apache2/sites-enabled/ | symlinks; a2ensite / a2dissite manage them |
/etc/apache2/mods-available/*.{load,conf} | module config – managed by a2enmod |
/var/www/html/ | default DocumentRoot |
/var/log/apache2/{access,error}.log | the first place to look when anything fails |
A small but worth-it tweak: increase logging detail temporarily during setup, then revert.
| |
6.2 MySQL
| |
The mysql_secure_installation wizard asks five questions. The right answers are:
- VALIDATE PASSWORD plugin: yes, level 2 (strong).
- Set root password: a 16+ character password from a password manager. Save it.
- Remove anonymous users: yes.
- Disallow root login remotely: yes – you will tunnel via SSH.
- Remove test database: yes.
Then verify:
| |
The caching_sha2_password trap
MySQL 8.0 changed the default authentication plugin from mysql_native_password to caching_sha2_password. Older PHP mysqli drivers, the mysql PHP extension, and a number of CMS installers cannot speak the new protocol and fail with The server requested authentication method unknown to the client. The right fix today is to upgrade the driver; the pragmatic fix when you cannot is to tell MySQL to use the old plugin per user:
| |
Do this for the application user only – never weaken root.
A starter my.cnf worth knowing about
Out of the box MySQL 8 ships with a tiny buffer pool. For a site that is even slightly busy this is the biggest single performance lever:
| |
Restart MySQL after editing. The buffer pool is single-handedly responsible for the difference between “every query hits disk” and “the working set lives in RAM”.
6.3 PHP
| |
Verify the bridge between Apache and PHP:
| |
You should see HTML, not PHP source. If you see source, the php module did not get enabled:
| |
Delete info.php after testing – it leaks the entire PHP configuration, including loaded extensions, file paths and disable_functions. It is the first thing an attacker grep s for.
| |
7. Defence in depth: hardening the public surface

A public LAMP server with default settings will be probed by automated scanners within minutes. Treat security as five concentric rings, each one buying time even when the one outside it fails.
7.1 Security group – the perimeter
Already covered in section 4. The rule of thumb: your security group should make the OS firewall feel redundant, and your OS firewall should make the security group feel redundant. Neither should be your only line.
7.2 OS hardening
| |
7.3 TLS with Let’s Encrypt
Once you have a domain pointed at your IP, getting a certificate is two commands:
| |
certbot --apache writes a new vhost on port 443, enables mod_ssl and mod_rewrite, and adds a 301 redirect from 80 to 443. It also drops a systemd timer that renews the cert before expiry; verify with:
| |
A modern TLS config does not just turn TLS on; it turns the bad parts off. After certbot, edit /etc/apache2/sites-available/example.com-le-ssl.conf and add:
| |
7.4 MySQL hardening
- Bind to
127.0.0.1only (default in modern packages, verify in/etc/mysql/mysql.conf.d/mysqld.cnf). - One database user per application, with
GRANTscoped to that database. - No
GRANT ALL ... TO root@'%'– ever. - Backups encrypted at rest if the data is sensitive.
7.5 Application hygiene
php-fpminstead ofmod_phpif you can – isolates PHP failures from the Apache process tree.expose_php = Offanddisplay_errors = Offin/etc/php/8.1/apache2/php.inifor production.- Whatever framework you deploy, check it has a security advisory feed and subscribe to it. CVEs in CMSes are the single largest source of compromised LAMP servers.
8. End-to-end deployment: Discuz!
Discuz! is worth using as a worked example because it exercises every weak point of a fresh LAMP install: file permissions, multiple writable directories, MySQL user creation, PHP extension requirements and a web-based installer that double-checks all of them.
8.1 Download
| |
8.2 Permissions – the part everyone gets wrong
Apache runs as www-data (Ubuntu) or apache (CentOS). The single rule: the user running Apache must own every file that PHP needs to write, and only those.
| |
Note that this is 775, not 777. If www-data already owns the directory, 775 lets the owner (web user) write while keeping o+r for the rest. chmod 777 is folk wisdom, not advice – it lets every user on the system write your application files, and on a shared server that is a privilege-escalation path.
8.3 Database account
| |
Two things to notice:
discuz.*– the grant is scoped to one database. If Discuz is ever compromised, the attacker cannot read your other applications’ tables.'discuz_user'@'localhost'– the host part is part of the identity. The same username from a different host is a different user. Connections via the unix socket count as'localhost'; TCP to127.0.0.1counts as'127.0.0.1'. Ifmysql_secure_installationleftlocalhostand127.0.0.1distinct, grant both.
8.4 Run the installer
Visit http://YOUR_PUBLIC_IP/install/. Three things happen:
- Environment check – PHP version, GD, mbstring, mysqli. If anything is missing:
sudo apt install -y php-<extension> && sudo systemctl reload apache2. - Permission check – the green ticks should appear next to
data/,config/,uc_server/data/,uc_client/data/. If not, recheck section 8.2. - Database details – host
localhost, namediscuz, userdiscuz_user, password as set above.
After install:
| |
9. The five failures that hit everyone
Failure 1 – “Connection refused”
Means: something between you and Apache is dropping the TCP SYN, or Apache is not listening.
| |
If 127.0.0.1 works but the public IP does not, the OS is fine – check the security group in the cloud console.
Failure 2 – “403 Forbidden” or “Index of /”
Means: Apache served the directory but did not find an index file, or could not read it.
| |
The fix is almost always chown -R www-data:www-data /var/www/html – you wget ed something as root, and the web user cannot read it.
Failure 3 – PHP source code visible in the browser
Means: Apache is serving .php as a static file because the PHP handler is not registered.
| |
This is a security incident, not just a misconfiguration – never leave the box exposed in this state.
Failure 4 – “Can’t connect to MySQL server on ’localhost'”
Means: MySQL is down, the socket has moved, or credentials are wrong.
| |
A common cause on small instances: MySQL was OOM-killed. dmesg | tail -50 will show Killed process ... mysqld. Either tune innodb_buffer_pool_size down or move to a larger instance.
Failure 5 – Discuz says “Directory not writable”
Means: the web user cannot write to one of the four required dirs.
| |
Resist the urge to chmod -R 777 /var/www. It will work, and it will hurt later.
10. Production essentials
10.1 Virtual hosts
Stop dumping everything in /var/www/html/ the moment you have more than one site. Per-site directories under /var/www/<sitename>/ and per-site vhost files keep the layout sane.
| |
| |
configtest before reload is the difference between a graceful change and a five-minute outage when you mistype a brace.
10.2 Backups that you actually test
A backup you have not restored is not a backup. The minimum:
| |
# crontab -e
0 3 * * * /usr/local/bin/db-backup.sh
Add an OSS sync once a week so a failed disk does not also lose your backups:
| |
And once a month, on a separate machine: gunzip < some_backup.sql.gz | mysql -u root -p test_restore and verify the row counts. The first time is always educational.
10.3 Observability
The Aliyun Cloud Monitor agent gives you CPU, memory, disk and bandwidth out of the box. The two extra signals worth wiring up yourself:
- Apache
mod_statusexposed on127.0.0.1:80/server-status– requests per second, busy workers, slow requests. - MySQL
performance_schemaqueries to find the slow queries (SELECT digest_text, count_star, avg_timer_wait FROM events_statements_summary_by_digest ORDER BY sum_timer_wait DESC LIMIT 10).
A weekly five-minute look at these will catch capacity problems weeks before they bite.
11. Two topologies, one app

Almost every successful LAMP site eventually hits the wall of the single-instance topology and has to decide whether to scale up (bigger ECS) or out (split tiers). The picture above is the destination of that decision.
Stay all-in-one when your peak traffic fits in one instance, your DB is small enough that the buffer pool covers the working set, and you have one engineer. The localhost connection between PHP and MySQL is faster than any network call you can buy, and the operational footprint is one OS to patch.
Split into three tiers when you need horizontal scale (more PHP workers behind an SLB), high availability (RDS gives you primary + standby for free), or you are spending more on your single ECS than two smaller ones plus an RDS. The classic Aliyun three-tier:
- SLB terminates TLS, fans out to the web tier.
- ECS x N running Apache + PHP, all stateless (sessions in Redis, uploads on OSS).
- RDS for MySQL as the single source of truth.
The cost roughly triples; the failure surface goes from “one box” to “many boxes plus a network”, which is genuinely harder to operate. Do not migrate just because the diagrams look impressive – migrate because the single instance is actually saturating.
12. Compiling MySQL from source (advanced)
You normally do not need to do this. Use the package manager unless you have a concrete reason – a build flag the package omits, a pinned version your vendor mandates, a patch the upstream has not merged. The downsides of source builds are real: hours of compile time, no automatic security updates, your own job to track CVEs.
If you do need it, the canonical incantation for MySQL 5.6 on CentOS:
| |
Two things go wrong almost every time:
Could not find OpenSSL– you missedopenssl-devel. Fix: install it, then remove the build directory and re-extract before retrying.cmakecaches partial state and a half-finished tree will not pick up the new headers.- OOM during compilation –
make -j$(nproc)on a 2 GiB instance will be killed. Use-j2and add 2 GiB of swap before starting.
After install, do not forget the same mysql_secure_installation and my.cnf tuning from section 6.2 – a from-source build is not configured for you.
13. Real-world cases
Case A – Migrating WordPress from shared hosting
The recipe that has worked dozens of times:
| |
The pitfall is almost always permissions – shared hosts give you ownership of everything; on ECS, www-data does, and uploads will silently fail until you fix it.
Case B – Two PHP versions on one box
Old plugin needs 5.6, new app wants 8.1. Use php-fpm per version and route by vhost:
| |
| |
This is also the right time to leave mod_php behind. php-fpm runs PHP in its own process pool, with its own user, its own resource limits, and its own crash recovery – a memory leak in PHP no longer takes Apache down.
Case C – A site that suddenly returns 502s under load
A common pattern: traffic doubles, the site starts returning 502 to about 5% of requests. The chain of cause is almost always:
- Apache prefork hits
MaxRequestWorkers; new connections queue. - PHP-FPM hits
pm.max_children; Apache gets a 502 from the FPM socket. - MySQL hits
max_connections; PHP-FPM workers block waiting for a connection, then time out.
The fix is to size each tier to one above the next. A starting point on a 4 vCPU / 16 GiB instance:
| |
The numbers are not magic; the principle is. Each layer’s worker pool must be able to absorb the layer above it without queueing for too long.
14. Summary
LAMP on Aliyun ECS reduces to a five-step recipe:
- Open ports correctly – security group, then OS firewall, then verify hop by hop.
- Install in order – Apache, MySQL, PHP, in that order, each one verified before moving on.
- Verify each layer – Apache serves HTML, PHP runs, MySQL connects. Three commands, every time.
- Set permissions deliberately –
www-dataowns the writable parts, nochmod 777. - Deploy the app – whether Discuz, WordPress or your own PHP, the playbook is the same.
What to do next:
- Add HTTPS with Let’s Encrypt and set HSTS.
- Wire up
mysqldump -> OSSand restore from backup at least once. - Read your access log for an hour – you will learn more about your traffic and your attackers than from any blog post.
- Once you outgrow one box, split into SLB + ECS + RDS rather than scaling the single instance forever.
Further reading:
- Apache HTTP Server documentation – https://httpd.apache.org/docs/
- MySQL 8.0 reference manual – https://dev.mysql.com/doc/refman/8.0/en/
- PHP manual – https://www.php.net/manual/en/
- Aliyun ECS user guide – https://www.alibabacloud.com/help/en/ecs/
- Let’s Encrypt with Certbot – https://certbot.eff.org/instructions