Ravindra BagaleCourses & study guides मराठी Track your progress

How to Create a Registration Form in PHP and MySQL

Every page we have served so far only asked the server for something. In this chapter we build a page that sends data to it: a registration form in HTML, a PHP script that receives the data, and a MariaDB table that stores it, all on one Amazon Linux EC2 server running nginx, php-fpm and MariaDB. You will also see how to get the code from an AI assistant, what you must change in what it gives you, and how to find out why a form does not work.

What you'll learn in this class

  • GET and POST, and why a form that sends data uses POST
  • An HTML form: action, method and the name of each input
  • $_POST, the array that holds the submitted values
  • The four database connection values, and why localhost is 127.0.0.1
  • PHP's MySQL driver php-mysqlnd, and restarting php-fpm
  • The insta database, the users table, and a database user just for the app
  • register.html and signup.php with a prepared statement and password_hash()
  • Testing with SELECT, and a login.php that uses password_verify()
  • Writing prompts for an AI assistant and fixing its placeholders
  • View Page Source, and a checklist for a form that does not work

1. GET and POST

Why: a registration form has to carry a name, an email and a password from the browser to the server. How the browser sends them decides whether the password stays private.

What: every request from a browser has a method. Two matter here:

  1. GET asks the server for something. Typing a URL, clicking a link and refreshing are all GET requests. Any data rides in the URL, after a ?.
  2. POST gives data to the server. The data travels inside the request body, so it never appears in the URL, the browser history or the server's access log.

Classroom line

In a POST request we aren't asking the server for something; we are giving something to it.

How it looks:

  1. You open http://SERVER-IP/register.html: a GET. The server answers with the form.
  2. You fill the form and click Register: a POST to signup.php, with name=...&email=...&password=... in the body.
  3. The same form without method="POST" falls back to GET, and the browser shows signup.php?name=Asha&email=asha%40example.com&password=MySecret123 in the address bar. Anyone looking at the screen, the history or the log can read the password.
GET asks the server for something; POST handsdata to it URL http://SERVER-IP/register.html Browser: Opens a page GET /register.html only a request, no data Server: nginx + PHP: Sends the file 200 OK + the form HTML GET = ask the server for something. Typing a URL, clicking a link, refreshing: every normal page visit is aGET. URL http://SERVER-IP/signup.php Name: Asha · Email: asha@example.com · Pass: ******** POST /signup.php body: name=Asha&email=... signup.php reads the data and saves it Registration successful. POST = give data to the server. The data travels inside the request body. The URL stays clean. http://SERVER-IP/signup.php?name=Asha &email=asha%40example.com &password=MySecret123 GET /signup.php?name=... ...&password=MySecret123 The password is now in the server access log A form without method=POST falls back to GET. Everything typed, the password too, lands in the URL, the browserhistory and the server logs.

Figure 1. Animation: GET asks for a page and carries no data; POST sends the form data inside the request body; a form without method=POST falls back to GET and puts the password in the URL.

2. The server: nginx, php-fpm and MariaDB

Why: three programs work together on one EC2 server, and all three must be running before the form can work.

What:

  1. nginx is the web server on port 80. It sends .html files itself and hands every .php file to php-fpm.
  2. php-fpm runs the PHP code.
  3. MariaDB is the database server on port 3306.

How:

  1. Connect to your Amazon Linux EC2 instance with ssh -i key.pem ec2-user@SERVER-IP.
  2. If the three are not installed yet, install them in one go: sudo yum install nginx mariadb105-server php -y.
  3. Start them:

    sudo service nginx start
    sudo service mariadb start
    sudo service php-fpm start
    
  4. Each one prints Redirecting to /bin/systemctl start nginx.service (or mariadb, php-fpm). That is normal: on Amazon Linux the service command passes the job to systemctl.

  5. Go to the folder nginx serves on Amazon Linux: cd /usr/share/nginx/html.
  6. Make two test pages:

    echo '<h1>HTML works</h1>' | sudo tee index.html
    echo '<?php phpinfo();' | sudo tee index.php
    
  7. Open http://SERVER-IP/index.html and http://SERVER-IP/index.php. The first shows HTML works; the second shows the long purple PHP information page.

Classroom line

Make index.html and index.php and check; both pages must open.

Classroom line

If both pages open, it means PHP and HTML are working properly.

  1. The PHP information page shows server details, so delete it once you have seen it: sudo rm /usr/share/nginx/html/index.php.
Three services on one server, plus onedriver Browser Shows the form, sends GET and POST nginx Web server, port 80 serves .html itself, passes .php on php-fpm Runs the PHP code. Needs php-mysqlnd to talk to MariaDB MariaDB Database server, port 3306, holds insta.users php-mysqlnd Start the three services sudo service nginx startsudo service mariadb startsudo service php-fpm start Add the driver once sudo yum install php-mysqlndsudo service php-fpm restartphp -m | grep -i mysql Redirecting to /bin/systemctl ... is normal: service passes thecommand to systemctl. Only php-fpm loads PHP extensions, so only php-fpm needs therestart; nginx does not.

Figure 2. nginx serves .html and passes .php to php-fpm; php-fpm needs the php-mysqlnd driver to talk to MariaDB. Start all three with sudo service ... start.

3. PHP's MySQL driver: php-mysqlnd

Why: PHP alone cannot talk to MariaDB. It needs a driver, an extension that knows the MySQL/MariaDB protocol. Without it, the mysqli functions do not exist, and signup.php stops with a blank page or 500 Internal Server Error.

What: on Amazon Linux the package is php-mysqlnd. It matches the PHP version you already have and adds mysqli and pdo_mysql.

How:

  1. Install it:

    sudo yum install php-mysqlnd -y
    
  2. Restart php-fpm so it loads the new extension:

    sudo service php-fpm restart
    
  3. Only php-fpm loads PHP extensions, so nginx and MariaDB need nothing.

  4. Check that the driver is there:

    php -m | grep -i mysql
    
  5. You should see mysqli, mysqlnd and pdo_mysql.

4. The database: insta, users and an app user

Why: the form needs a table to land in, and the PHP code needs a database login. That login should not be root: if the app's password ever leaks, the damage must stay limited to what the app really needs.

How:

  1. Log in with mysql -u root -p.
  2. Create the database and the table:

    CREATE DATABASE insta;
    USE insta;
    CREATE TABLE users (
      id       INT AUTO_INCREMENT PRIMARY KEY,
      name     VARCHAR(100) NOT NULL,
      email    VARCHAR(150) NOT NULL UNIQUE,
      password VARCHAR(255) NOT NULL
    );
    
  3. One column per form field, plus id that numbers the rows by itself.

  4. email is UNIQUE, so one email can register only once.
  5. password is VARCHAR(255) because it will hold a hash (section 8), not the typed password. A hash today is 60 characters; 255 leaves room for stronger ones later.
  6. Create a user just for the app, with your own password in place of the placeholder:

    CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'YOUR_APP_PASSWORD';
    GRANT SELECT, INSERT ON insta.users TO 'appuser'@'localhost';
    
  7. appuser can read and add rows in insta.users, and nothing else. It cannot drop the table or look at other databases.

  8. Check with SHOW GRANTS FOR 'appuser'@'localhost';. Try DROP TABLE users; while logged in as appuser and you get ERROR 1142 ... DROP command denied.
  9. exit;

5. The four connection values

Why: to open a connection, PHP needs to know where the database is, who is logging in, with which password, and which database to use. If even one value is wrong, nothing gets saved.

What:

  1. Server name: where MariaDB runs. Here it is on the same EC2 server as PHP, so localhost.
  2. User name: appuser.
  3. Password: the one you set in CREATE USER.
  4. Database: insta.

Classroom line

The database is on the same server as PHP, so we call it localhost.

Classroom line

localhost is a domain name; its IP is 127.0.0.1.

How: in PHP the four values are four variables, and one line opens the connection:

$servername = "localhost";
$username   = "appuser";
$password   = "YOUR_APP_PASSWORD";
$database   = "insta";
$conn = new mysqli($servername, $username, $password, $database);
  1. localhost always means "this same machine". Its address is 127.0.0.1.
  2. With localhost, mysqli connects through MariaDB's local socket file. With 127.0.0.1 it uses TCP port 3306. Both reach the same database.
  3. If the database ever moves to its own server (for example Amazon RDS), only $servername changes, to that server's address.
Four values PHP needs to reach the database $servername = "localhost"; Where MariaDB runs. Same server as PHP, so localhost (its IP is127.0.0.1). $username = "appuser"; A database user made only for this app. Not root. $password = "YOUR_APP_PASSWORD"; That user password. A placeholder here: put the one you set inCREATE USER. $database = "insta"; The database that holds the users table. Check with SHOWDATABASES; $conn = new mysqli($servername, $username, $password,$database); One EC2 server php-fpm signup.php localhost · 127.0.0.1 MariaDB insta.users One wrong value and the connection fails: Access denied for user, orUnknown database.

Figure 3. The four connection values: localhost (127.0.0.1, the same server), a user just for the app, its password, and the database insta. One wrong value and the connection fails.

6. The HTML form: action, method and name

Why: three attributes decide where the form data goes, how it travels, and what it is called when it gets there.

What:

  1. action="signup.php": the file that receives the data. It must match the PHP file name exactly, including upper and lower case.
  2. method="POST": send the data in the request body.
  3. name="..." on every input: the label each value carries. PHP finds the value by this name.

Classroom line

If you write signup.php here and put the PHP code in a different file, it won't work.

How:

  1. Create the form: sudo nano /usr/share/nginx/html/register.html

    <!DOCTYPE html>
    <html>
    <head><meta charset="utf-8"><title>Register</title></head>
    <body>
      <h2>Create Account</h2>
      <form action="signup.php" method="POST">
        <label>Name</label>     <input type="text" name="name" required><br>
        <label>Email</label>    <input type="email" name="email" required><br>
        <label>Password</label> <input type="password" name="password" minlength="8" required><br>
        <button type="submit">Register</button>
      </form>
    </body>
    </html>
    
  2. type="password" shows dots instead of the letters. required and minlength="8" make the browser check the fields before sending.

  3. Open http://SERVER-IP/register.html. A version with some CSS styling looks like the screenshot below; the form tag and the input names are what matter.
A Create Account form with three labelled boxes, Name (Enter your name), Email (Enter your email) and Password (Enter your password), and a blue Register button.

Whiteboard: The registration form served by nginx: Name, Email and Password fields and a Register button.

  1. Want to see the HTML of any page, yours or someone else's? Right-click the page and choose View Page Source (or press Ctrl+U). You can read it, and copy it as a starting point.

Classroom line

Want this same code? Right-click on the page, choose View Page Source and copy it.

  1. In the source, find the <form ...> line and check action and method first.
Browser source view, lines 80 to 96: body, div class register-box, h2 Create Account, then line 85 form action="signup.php" method="POST", followed by a form-group div with label for name and an input of type text, id name, name name, placeholder Enter your name, required.

Whiteboard: View Page Source of the registration page: the form tag sends the data with method POST to signup.php.

7. $_POST: the form data inside PHP

Why: when the POST arrives, signup.php needs a way to get at each value.

What: PHP puts everything that came in a POST body into $_POST, an array. An array is one variable that holds many values, each under its own key. The keys are the name attributes from the form.

Classroom line

An array is a variable that holds multiple values inside it.

How:

  1. For our form, $_POST holds three entries: 'name' => 'Asha', 'email' => 'asha@example.com', 'password' => 'MySecret123'.
  2. Take each value out by its key:

    $name  = trim($_POST["name"] ?? "");
    $email = trim($_POST["email"] ?? "");
    $pass  = $_POST["password"] ?? "";
    
  3. ?? "" gives an empty string if the field is missing, instead of a warning.

  4. trim() removes spaces typed by mistake before or after the name and email.
  5. If the key in PHP is spelt differently from the name in HTML ($_POST["Email"] for name="email"), the value is simply empty.
The name of each input becomes a key in $_POST In register.html <form action="signup.php" method="POST"> <input type="text" name="name"> <input type="email" name="email"> <input type="password" name="password"> </form> Sent in the request body name=Asha email=asha%40example.com password=MySecret123 What signup.php receives $_POST = [ 'name' => 'Asha', 'email' => 'asha@example.com', 'password' => 'MySecret123', ]; Take each value out by its key $name = $_POST['name']; $email = $_POST['email']; $pass = $_POST['password']; An array is one variable that holds many values, each under itsown key. A spelling difference between name=... in HTML and the key in PHP gives anempty value.

Figure 4. Each input's name becomes a key in $_POST: name=Asha in the request body becomes $_POST['name'] in signup.php.

8. signup.php, the safe way

Why: this file decides what lands in your database. Two rules make it safe:

  1. Never paste form data into the SQL text. If you build the query as "... VALUES ('$name', ...)", a quote typed in the name field ends your string and the rest runs as SQL. This is called SQL injection. A prepared statement sends the SQL first, with ? for each value, and the values separately, so they can only ever be data.
  2. Never store the password itself. password_hash() turns it into a one-way hash. Anyone who reads the table, a backup or a leaked copy sees only hashes.

Classroom line

Understanding it matters more than just doing it; pay attention.

How: sudo nano /usr/share/nginx/html/signup.php

<?php
// 1. The four connection values
$servername = "localhost";          // same server as PHP (127.0.0.1)
$username   = "appuser";            // limited user, not root
$password   = "YOUR_APP_PASSWORD";  // placeholder: put your own
$database   = "insta";

// Only accept form submissions (POST)
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
    header("Location: register.html");
    exit;
}

// 2. Read the form fields from $_POST
$name  = trim($_POST["name"] ?? "");
$email = trim($_POST["email"] ?? "");
$pass  = $_POST["password"] ?? "";

if ($name === "" || !filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($pass) < 8) {
    exit("Please fill all fields. Password must be at least 8 characters.");
}

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
    // 3. Connect
    $conn = new mysqli($servername, $username, $password, $database);

    // 4. Store a hash, never the plain password
    $hash = password_hash($pass, PASSWORD_DEFAULT);

    // 5. Prepared statement: data goes separately from the SQL
    $stmt = $conn->prepare("INSERT INTO users (name, email, password) VALUES (?, ?, ?)");
    $stmt->bind_param("sss", $name, $email, $hash);
    $stmt->execute();

    echo "Registration successful. <a href=\"login.html\">Log in</a>";
} catch (mysqli_sql_exception $e) {
    if ($e->getCode() == 1062) {
        echo "An account with this email already exists.";
    } else {
        error_log("signup.php: " . $e->getMessage());
        echo "Something went wrong. Please try again later.";
    }
}

Read it top to bottom:

  1. The four connection values from section 5.
  2. A GET to signup.php (someone typing its URL) is sent back to the form.
  3. The three values come out of $_POST, and are checked: a name, a valid email, a password of at least 8 characters. The browser checks too, but anyone can skip the browser, so the server checks again.
  4. mysqli_report(...) makes every database error an exception that the catch block handles.
  5. password_hash($pass, PASSWORD_DEFAULT) makes the hash, a 60-character text that starts with $2y$.
  6. prepare() sends the SQL with three ?. bind_param("sss", ...) attaches three strings, one per ?, in order. execute() runs it.
  7. Error 1062 is the duplicate entry from the UNIQUE email: the user gets a clear message.
  8. Any other error goes to the PHP error log for you, while the visitor sees a general message. Database details are never shown on the page.
Never paste form data into SQL: use a preparedstatement Pasted into the SQL text $sql = "INSERT INTO users (name, email, password)VALUES ('$name', '$email', '$pass')"; $conn->query($sql); Someone types this as the name: x'); DROP TABLE users; -- The quote ends your string and the rest runs as SQL. A plain passwordis stored as typed, too. Prepared statement $stmt = $conn->prepare("INSERT INTO users (name, email,password) VALUES (?, ?, ?)"); $hash = password_hash($pass, PASSWORD_DEFAULT); $stmt->bind_param("sss", $name, $email, $hash); $stmt->execute(); The SQL is fixed first, with ? for each value. The values travelseparately, so the same name is stored as plain text in the name column. sss: three strings, one per ?. Tested: that name was saved as text, and the users table was stillthere.

Figure 5. Pasted SQL lets a typed quote change the command; a prepared statement fixes the SQL first and sends the values separately, so the same text is stored as a plain name.

Store a hash, never the password itself Stored as typed (wrong) 1 Asha asha@example.com MySecret123 Anyone who can read the table sees every password: a copied backup, a leaked dump, a curious admin. People reusepasswords, so one leak opens their email and bank accounts too. signup.php stores a hash $hash = password_hash('MySecret123', PASSWORD_DEFAULT); // $2y$12$GscnGr53N2GXU/ebcR2WgOk... (60 characters) 1 Asha asha@example.com $2y$12$GscnGr53N2GXU/ebcR... One way: the hash cannot be turned back into the password. The samepassword hashed twice gives different text (a random salt is inside).Keep the column VARCHAR(255). login.php checks with password_verify password_verify('MySecret123', $user['password']) //true -> Welcome, Asha! password_verify('wrong', $user['password']) // false ->Wrong email or password. Fetch the row by email with a prepared statement, then letPHP compare. Never compare with WHERE password = ...: the stored text is a hash,not the password. Show one message for a wrong email and a wrong password.

Figure 6. Animation: a password stored as typed is readable by anyone with the table; password_hash() stores a one-way 60-character hash; password_verify() checks a login against it.

9. Test the whole flow

Why: a form that "looks fine" proves nothing. Follow one registration all the way into the table.

How:

  1. Open http://SERVER-IP/register.html, fill in Asha, asha@example.com and a password of 8 or more characters, and click Register.
  2. The page says Registration successful. Log in. The address bar shows just /signup.php, no data.
  3. Go back and register the same email again. The page says An account with this email already exists.
  4. Check from the database side with mysql -u root -p:

    USE insta;
    SELECT id, name, email, password FROM users;
    
  5. Asha's row is there, and the password column holds something like $2y$12$GscnGr53N2GXU/ebcR2Wg..., not the password.

  6. Register a second person. Their id is 3, not 2: the failed duplicate insert used up id 2, as with any failed insert into an AUTO_INCREMENT table.
  7. Type a name like x'); DROP TABLE users; -- and register. It is saved as a plain name, and users is still there. That is the prepared statement at work.
One registration, start to finish 1. The browser asks nginx for register.html. Browser → nginx /usr/share/nginx/html nginx finds it in its web root and sends the form. Plain .html files nevertouch PHP. GET /register.html -> 200 OK, the Create Account form 2. Register: the browser sends POST /signup.php. nginx → php-fpm signup.php nginx hands every .php file to php-fpm, which runs signup.php with theform data in $_POST. $_POST['name'] = 'Asha' $_POST['email'] ='asha@example.com' $_POST['password'] = '...' 3. signup.php connects and inserts the row with a preparedstatement. php-fpm → MariaDB insta.users MariaDB stores it; the browser gets Registration successful. | id | name | email | password (hash) | | 1 | Asha | asha@example.com |$2y$12$GscnGr53N2GXU/ebcR... | Check it from the database side: SELECT * FROM users; showsthe new row.

Figure 7. Animation: the browser gets register.html from nginx; Register sends POST /signup.php, which nginx passes to php-fpm; signup.php inserts the row into insta.users in MariaDB.

10. The next step: login.php

Why: registration stores users; a login page checks them. It uses the same pieces in reverse.

What: login.html is the same form with only email and password, and action="login.php". login.php looks up the row by email and lets PHP compare the typed password with the stored hash.

How: sudo nano /usr/share/nginx/html/login.php

<?php
$servername = "localhost";
$username   = "appuser";
$password   = "YOUR_APP_PASSWORD";
$database   = "insta";

if ($_SERVER["REQUEST_METHOD"] !== "POST") {
    header("Location: login.html");
    exit;
}

$email = trim($_POST["email"] ?? "");
$pass  = $_POST["password"] ?? "";

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
    $conn = new mysqli($servername, $username, $password, $database);

    $stmt = $conn->prepare("SELECT name, password FROM users WHERE email = ?");
    $stmt->bind_param("s", $email);
    $stmt->execute();
    $user = $stmt->get_result()->fetch_assoc();

    if ($user && password_verify($pass, $user["password"])) {
        echo "Welcome, " . htmlspecialchars($user["name"]) . "!";
    } else {
        echo "Wrong email or password.";
    }
} catch (mysqli_sql_exception $e) {
    error_log("login.php: " . $e->getMessage());
    echo "Something went wrong. Please try again later.";
}
  1. The query finds the row by email only. Never write WHERE password = '...': the column holds a hash, not the password.
  2. password_verify($pass, $user["password"]) hashes the typed password the same way and compares. True means the password is right.
  3. A wrong email and a wrong password get the same message, so nobody can use the form to find out which emails are registered.
  4. htmlspecialchars() prints the name safely, even if it contains < or quotes.
  5. Test: the right password gives Welcome, Asha!; a wrong one gives Wrong email or password.
  6. A real app would now start a session (session_start()) to remember the logged-in user from page to page.

11. Let an AI assistant write the code, then fix it

Why: as a cloud engineer you will deploy, configure and debug applications you did not write. AI assistants such as ChatGPT write this kind of code in seconds, but they cannot know your server, your database or your passwords. Your job is to ask clearly and then read and correct every line.

Classroom line

Whatever half-information we give ChatGPT, that's all we'll get back from it.

How to write the prompts:

  1. One file per prompt, and name everything. For the form: Create a registration form in HTML with name, email and password fields that sends the data with POST to signup.php.
  2. Then the script, with your real names and the safety rules: Create signup.php for it: PHP mysqli, database insta, table users with columns name, email and password, prepared statements, password_hash(), and a message if the email already exists.
  3. A vague prompt such as make a signup page makes the assistant guess the file name, the fields, the database and the table, and you spend longer fixing the guesses.

Classroom line

You must learn how to prompt.

What to change in what you get:

  1. The four connection values. The assistant writes placeholders such as your_username, your_database or enter your password. They never work as they are.
  2. Database, table and column names. Copy them from SHOW DATABASES; and DESC users;.
  3. The action of the form must match the PHP file name you actually save.
  4. Safety: if the code builds SQL by pasting variables into a string, or stores $_POST["password"] directly, ask for a prepared statement and password_hash(), or change it yourself.
  5. Test, then SELECT * FROM users;.

Classroom line

ChatGPT doesn't give you this; it just says 'enter your password'.

Keep the code safe:

  1. A .php file in the web root is run, not shown, so its database password stays on the server.
  2. Never leave a copy such as signup.txt or signup.php.bak in /usr/share/nginx/html: nginx sends those as plain text, password and all. To share code, copy it from the terminal or with scp.
  3. Never paste your real database password into an AI chat. Use the placeholder and fill it in on the server.
Using an AI assistant: a complete prompt, then fixthe placeholders Vague prompt make a signup page The assistant guesses the file name, the fields, the database and thetable. Half the information, half the result. Complete prompts, one file at a time Create a registration form in HTML with name, email andpassword that sends the data with POST to signup.php. Create signup.php for it: PHP mysqli, database insta,table users(name, email, password), preparedstatements, password_hash(), and an error if the emailexists. Then edit what the assistant cannot know 1. The four connection values 2. Database, table, column names 3. action matches the file name 4. Test, then SELECT * FROM users; Placeholders such as enter your password or your_database never work asthey are. Read every line before you save it.

Figure 8. A vague prompt leaves the assistant guessing; complete prompts name the files, fields, database and safety rules. Then fix the connection values, the names and the action, and test.

12. Why does my form not work?

Why: almost every broken form comes down to a few small things. Check them in this order.

Classroom line

These are very small things: a spelling mistake, the wrong folder, a wrong password.

  1. 404 Not Found for register.html: the file is in the wrong folder. nginx on Amazon Linux serves /usr/share/nginx/html. /var/www/html is Apache's folder (and nginx's on Ubuntu), so a file there gives 404 here. Check with ls /usr/share/nginx/html.
  2. Register does nothing, or 405 Not Allowed: the form has no action. Without it the browser sends the data back to register.html itself; with method="POST", nginx refuses a POST to a plain .html file with 405. Add action="signup.php".
  3. The typed data, password too, appears in the URL: the form has no method, so it uses GET. Add method="POST".

Classroom line

It's sending the data in the URL; change the HTML and use method.

  1. 404 Not Found for signup.php: the file name differs from the action: signup.html, Signup.php, sigup.php. Linux file names are case-sensitive.
  2. 500 or a blank page: look in the PHP error log. Class "mysqli" not found or Call to undefined function mysqli_... means the driver is missing: sudo yum install php-mysqlnd -y, then sudo service php-fpm restart.
  3. Something went wrong, and the log says Access denied for user 'appuser'@'localhost' or Unknown database: a connection value does not match what you created in MariaDB.
  4. The log says Unknown column 'fullname' or Table 'insta.user' doesn't exist: the INSERT uses names that are not in your table. Copy them from DESC users;.
  5. Where the logs are: nginx writes to /var/log/nginx/error.log (missing files, 404s); PHP errors from php-fpm go to /var/log/php-fpm/www-error.log. Read the last lines with sudo tail -n 20 and the file name.
Why does my form not work? Check in this order 404 Not Found for register.html Cause Wrong folder, e.g. /var/www/html Fix Put it in /usr/share/nginx/html Nothing happens, or 405 Not Allowed Cause The form has no action Fix action=signup.php on the form tag The password shows in the URL Cause The form has no method, so GET Fix method=POST on the form tag 404 Not Found for signup.php Cause File name differs from the action Fix Same spelling and case: signup.php 500 error or a blank page Cause PHP MySQL driver missing (mysqli) Fix yum install php-mysqlnd, restart php-fpm Access denied / Unknown database Cause Connection values do not match Fix Use the values set in MariaDB Unknown column / Table doesn't exist Cause Wrong table or column names Fix Copy the names from DESC users; Still stuck? Read the logs: /var/log/nginx/error.log and/var/log/php-fpm/www-error.log

Figure 9. Why a form does not work, in order: wrong folder, no action, no method, file name not matching the action, missing driver, wrong connection values, wrong table or column names. The logs tell you which.

Ravindra Bagale's Tip

Test every layer on its own before you test the form. index.html opens: nginx is fine. index.php opens: php-fpm is fine. mysql -u appuser -p insta logs in: the user, password and database are right. Only then click Register. When something breaks, sudo tail -n 20 /var/log/nginx/error.log and /var/log/php-fpm/www-error.log usually name the exact problem in one line.

When you're done practising, delete any test pages you no longer need and stop the EC2 instance.

13. Hands-on lab: one registration, from the form to the table

Why this lab. A form has many links: nginx, php-fpm, the driver, the database, the app user, the HTML and the PHP. This lab checks each link in order, so when a form breaks you know which link to test.

  1. Start the three services
    • Command: sudo service nginx start; sudo service mariadb start; sudo service php-fpm start
    • You should see: Three Redirecting to /bin/systemctl start ... lines.
    • Check: sudo service php-fpm status shows active (running); the same for nginx and mariadb.
    • Why: nginx receives the request, php-fpm runs the PHP, MariaDB stores the row. One stopped service breaks the whole form.
  2. Install the MySQL driver for PHP
    • Command: sudo yum install php-mysqlnd -y && sudo service php-fpm restart
    • You should see: Complete! and the restart line.
    • Check: php -m | grep -i mysql lists mysqli, mysqlnd and pdo_mysql.
    • Why: PHP alone can't talk to MariaDB. php-fpm loads extensions only at start, hence the restart.
  3. Create the database, table and app user
    • Do: Log in with sudo mysql -u root -p and run the CREATE DATABASE insta;, CREATE TABLE users (...), CREATE USER 'appuser'@'localhost' ... and GRANT SELECT, INSERT ON insta.users ... statements from section 4.
    • Check: SHOW GRANTS FOR 'appuser'@'localhost'; lists SELECT, INSERT on insta.users.
    • Why: The app logs in as a user that can only read and add rows in one table. If the code is ever abused, it can't drop tables or read other databases.
  4. Test the app login from the terminal
    • Command: mysql -u appuser -p insta -e "SELECT COUNT(*) FROM users;"
    • You should see: After the app password: a small table with 0.
    • Check: No Access denied.
    • Why: If these four values (host, user, password, database) fail here, they will fail in PHP too. Testing here first separates database problems from code problems.
  5. Put the form and signup.php in place
    • Do: Create register.html (section 6) and signup.php (section 8) in /usr/share/nginx/html with sudo nano.
    • Check: php -l /usr/share/nginx/html/signup.php → No syntax errors detected.
    • Why: action="signup.php" must match the file name exactly. php -l catches a missing semicolon before a visitor gets a blank page.
  6. Register one user
    • Do: Open http://PUBLIC_IP/register.html, fill in a name, an email and a password, and submit.
    • You should see: The success message from signup.php, and the address bar shows /signup.php with no form data in it.
    • Check: No 500 Internal Server Error and no blank page.
    • Why: With method="POST" the data travels in the request body, not in the URL, so the password never shows in the address bar or the browser history.
  7. Check the row
    • Command: sudo mysql -u root -p insta -e "SELECT id, name, email, password FROM users;"
    • You should see: One row with your name and email; the password column starts with $2y$.
    • Check: The password you typed does not appear anywhere.
    • Why: password_hash() stores a hash, never the password. If the table leaks, the real passwords don't.
  8. Stop the instance
    • Do: EC2 console → Instance state → Stop instance.
    • Check: State Stopped.
    • Why: Three services run all the time on this server. Stopping saves free-tier hours; the database stays on the disk.

14. The whole class on one page

  1. GET asks for something and carries data in the URL; POST gives data to the server inside the request body.
  2. A form needs action="signup.php" (the receiving file), method="POST", and a name on every input.
  3. $_POST is an array: $_POST["email"] is the value of the input named email.
  4. Start the stack with sudo service nginx|mariadb|php-fpm start; nginx on Amazon Linux serves /usr/share/nginx/html.
  5. PHP needs php-mysqlnd to talk to MariaDB; after installing it, restart only php-fpm.
  6. users(id, name, email UNIQUE, password VARCHAR(255)), and an appuser with GRANT SELECT, INSERT ON insta.users, never root.
  7. Four connection values: localhost (127.0.0.1), user, password, database.
  8. Prepared statement: prepare("... VALUES (?, ?, ?)"), bind_param("sss", ...), execute().
  9. password_hash() when storing, password_verify() when logging in.
  10. Check with SELECT * FROM users;. A duplicate email gives error 1062 and uses up an id.
  11. AI assistants: complete prompts, then fix the placeholders, the names and the action; never paste real passwords.
  12. A broken form: folder, action, method, file name, driver, connection values, table and column names, then the logs.

15. Try at home

Try at home

Task 1: the stack

  1. Start nginx, MariaDB and php-fpm, and open an index.html and an index.php from /usr/share/nginx/html.
  2. Install php-mysqlnd, restart php-fpm, and confirm mysqli with php -m.

Task 2: registration

  1. Create insta.users and appuser with only SELECT and INSERT.
  2. Build register.html and signup.php, register two users, and look at the hashes with SELECT.
  3. Register the same email twice and note the id of the next new user.

Task 3: break it on purpose

  1. Remove method="POST" from the form and register: where did the password go?
  2. Remove the action, then rename signup.php to Signup.php. Note each error.
  3. Put a wrong password in $password and find the message in /var/log/php-fpm/www-error.log.

Task 4: login and an extra field

  1. Build login.html and login.php with password_verify(), and test a right and a wrong password.
  2. Ask an AI assistant to add a phone field to both the form and signup.php. Add the column with ALTER TABLE, fix every placeholder, and test.

Recap

In short

  1. GET asks the server for something; POST sends data in the request body, so a form with a password always uses method="POST".
  2. The form's action names the PHP file that receives the data, and each input's name becomes a key in the $_POST array.
  3. nginx serves /usr/share/nginx/html and hands .php to php-fpm; php-mysqlnd lets PHP reach MariaDB, and only php-fpm needs a restart after installing it.
  4. PHP connects with four values: localhost (127.0.0.1), a user just for the app, its password and the database insta.
  5. signup.php uses a prepared statement and password_hash(); login.php finds the row by email and checks it with password_verify().
  6. Test with SELECT * FROM users; and try a duplicate email.
  7. Give an AI assistant complete prompts, then replace its placeholders and check every name.
  8. A broken form is usually the folder, the action, the method, a file name, the driver, a connection value or a column name; the nginx and php-fpm logs say which.

Practice Tasks

About this scenario

Pune Fintech Labs is a finance business in Pune. Rohan Kulkarni looks after its IT and cloud work. They count money in INR and run their AWS resources in the ap-south-1 Region. In the tasks below you do real jobs for them using what this lesson teaches. All names, account IDs, IP addresses and numbers are made up for practice.

0 / 12

Easy Restart a service after a change

Why: Config changes only apply after the service restarts or reloads.

What to do: Restart the nginx service.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Easy Check if a service is running

Why: When a site is down, first check whether the web server process is running.

What to do: Show the status of the httpd service.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Easy Install a package on Ubuntu

Why: Ubuntu uses apt. Package names differ: on Ubuntu Apache is apache2.

What to do: Install the apache2 package on Ubuntu without being asked to confirm.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Easy Install a package on Amazon Linux

Why: dnf is the package manager on Amazon Linux 2023. -y answers yes for you.

What to do: Install the httpd package on Amazon Linux 2023 without being asked to confirm.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Easy Biggest orders first

Why: Querying the database is daily work after you set up RDS.

What to do: Show order id and amount, biggest amount first.

Tables: customers(id, name, city) and orders(id, customer_id, amount, odate).

How:
  1. Find the table the task is about (see the table list above the box).
  2. Write the SQL statement step by step: what, from where, which rows.
  3. End the statement with ;
  4. Click Check answer. Your query runs on a practice copy of the table.

Easy Filter customers by city

Why: Querying the database is daily work after you set up RDS.

What to do: List the names of customers in Pune.

Tables: customers(id, name, city) and orders(id, customer_id, amount, odate).

How:
  1. Find the table the task is about (see the table list above the box).
  2. Write the SQL statement step by step: what, from where, which rows.
  3. End the statement with ;
  4. Click Check answer. Your query runs on a practice copy of the table.

Medium Test the nginx config

Why: A test catches typos before a restart takes your site down.

What to do: Check the nginx configuration files for errors.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Medium Start a service now and at boot

Why: enable makes it start after reboot; --now also starts it right away.

What to do: Enable the nginx service and start it immediately, in one command.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Medium Open port 80 to the world

Why: Visitors can reach your website only if the security group allows HTTP.

What to do: Allow inbound TCP port 80 from 0.0.0.0/0 on security group sg-00542045bb534c3b2.

How:
  1. Read the task and decide which AWS service it is about.
  2. Write aws, then the service name, then the operation (the action word).
  3. Add each needed flag (--name) followed by its value from the task.
  4. Flags can be in any order. Click Check answer.

Medium Total sales

Why: Querying the database is daily work after you set up RDS.

What to do: Show the total of all order amounts as one number.

Tables: customers(id, name, city) and orders(id, customer_id, amount, odate).

How:
  1. Find the table the task is about (see the table list above the box).
  2. Write the SQL statement step by step: what, from where, which rows.
  3. End the statement with ;
  4. Click Check answer. Your query runs on a practice copy of the table.

Medium Orders with customer names

Why: Querying the database is daily work after you set up RDS.

What to do: Show each customer name with each order amount.

Tables: customers(id, name, city) and orders(id, customer_id, amount, odate).

How:
  1. Find the table the task is about (see the table list above the box).
  2. Write the SQL statement step by step: what, from where, which rows.
  3. End the statement with ;
  4. Click Check answer. Your query runs on a practice copy of the table.

Hard Sales per customer

Why: Querying the database is daily work after you set up RDS.

What to do: Show customer_id and total amount per customer, only where total is above 1000.

Tables: customers(id, name, city) and orders(id, customer_id, amount, odate).

How:
  1. Find the table the task is about (see the table list above the box).
  2. Write the SQL statement step by step: what, from where, which rows.
  3. End the statement with ;
  4. Click Check answer. Your query runs on a practice copy of the table.

Revision, comparison and interview cards

Revise this lesson in a few minutes. Save the cards on your phone or download them as PNG files. The QR code on each card opens this lesson.

Revision card: GET vs POST in forms. A sign-up form sends passwords. They must not appear in the URL. GET puts data in the URL. POST sends it in the request body. Use method="post" on forms that send or change data. Form:
Revision card: GET vs POST in forms · Download PNG
Revision card: $_POST in PHP. PHP must read what the user typed in the form. $_POST is an array of submitted values, keyed by each input's name. Give every input a name, then read $_POST['name']. Input and PHP:
Revision card: $_POST in PHP · Download PNG
Revision card: Prepared statements. Gluing user input into SQL lets attackers run their own SQL (SQL injection). A prepared statement sends SQL with ? placeholders, and the values separately. prepare, bind_param, execute. SQL:
Revision card: Prepared statements · Download PNG
Revision card: Store passwords with password_hash. If the database leaks, plain passwords are stolen at once. password_hash makes a one-way hash. password_verify checks a login against it. Hash on sign-up, verify on login. Never store the plain text. PHP:
Revision card: Store passwords with password_hash · Download PNG
Comparison: GET vs POST. GET: Asks the server for something; data rides in the URL. POST: Sends data to the server in the request body.
Comparison: GET vs POST · Download PNG
Interview card: Why should a login form use POST, not GET?. GET puts the password in the URL, browser history and server logs. POST sends it in the body, and with HTTPS it is encrypted. GET leak: /login.php?pass=Secret123.
Interview card: Why should a login form use POST, not GET? · Download PNG
Interview card: What is SQL injection and how do you prevent it?. Input that changes your SQL, like ' OR 1=1 --. It can log in without a password or dump the table. Use prepared statements with ? placeholders.
Interview card: What is SQL injection and how do you prevent it? · Download PNG
Interview card: Why hash passwords instead of encrypting them?. A hash cannot be reversed; encryption can be decrypted with the key. Even you never need to read the password, only check it. PHP:
Interview card: Why hash passwords instead of encrypting them? · Download PNG
Interview card: A form submits but nothing is saved. What do you check?. action and method, input names, DB credentials, php-mysqlnd, and the PHP error log. Each link in the chain can fail silently. Log:
Interview card: A form submits but nothing is saved. What do you check? · Download PNG

More cards: Ravi Sir's Cards.