PHP — Blueprint
REST API and HTML frontend with plain PHP 8.3, nginx, and PostgreSQL
REST API backend and HTML frontend built with plain PHP 8.3, nginx, and PostgreSQL — no framework. Intended as a starting point for Kubernetes-deployed services: one table, one API endpoint, full Docker support out of the box.
Tech stack:
| Layer | Technology |
|---|---|
| Language | PHP 8.3 |
| Web server | nginx + PHP-FPM |
| Database | PostgreSQL (PDO) |
| Migrations | Custom runner (plain SQL) |
The application runs on port 80. The root / serves an HTML page with a space targets table; the API is available under /api/.
Project Structure
public/
└── index.php # Front controller: matches URI to handler
src/
├── db.php # PDO singleton (DATABASE_URL or DB_*)
├── migrate.php # Reads and executes SQL from migrations/
└── handlers/
├── home.php # GET / — HTML page with inline CSS + JS
├── targets.php # GET /api/targets — JSON response
└── info.php # GET /api/info — JSON response
migrations/
└── V1__init.sql # Creates space_target table + seed data
nginx/
└── default.conf # nginx config: passes .php requests to PHP-FPM
entrypoint.sh # Runs migration, starts php-fpm + nginxKey design decisions:
public/index.phpis the single entry point. It uses PHP 8matchto route byREQUEST_URIto handler files. To add a new endpoint, add amatcharm and create the corresponding file undersrc/handlers/.src/db.phpcreates a PDO instance once (static variable) and reuses it across all requests in the same FPM worker. It parsesDATABASE_URL(thepostgres://format) or falls back to individualDB_*variables.entrypoint.shrunssrc/migrate.phpbefore starting the server. Migrations are plain SQL files inmigrations/; the runner tracks executed files in aschema_migrationstable.- The Dockerfile is a single-stage build: it installs nginx, PHP-FPM, and the
pdo_pgsqlextension in one Alpine image, then copies the app. There is no compiled artifact — PHP files are served directly. - The HTML page in
src/handlers/home.phpincludes inline CSS and JavaScript. The JS fetches/api/infoand/api/targetson load and renders the table.
Environment Variables
The application runs on port 80 and requires a PostgreSQL database.
Database connection is configured via one of two approaches:
Option A — Single connection URL (recommended for managed services)
| Variable | Required | Example |
|---|---|---|
DATABASE_URL | yes | postgres://user:pass@host:5432/dbname?sslmode=require |
Option B — Individual variables
| Variable | Required | Default | Description |
|---|---|---|---|
DB_HOST | no | localhost | Database host |
DB_PORT | no | 5432 | Database port |
DB_NAME | no | postgres | Database name |
DB_USER | no | postgres | Database user |
DB_PASSWORD | no | (empty) | Database password |
DATABASE_URL takes precedence. If it is set, the individual DB_* variables are ignored.
Running Locally
With Docker Compose
docker compose upStarts the application together with a PostgreSQL instance and sets DATABASE_URL automatically. No additional configuration needed.
Standalone
Requires PHP 8.3 with pdo_pgsql, and a running PostgreSQL instance.
# run migrations
DATABASE_URL=postgres://postgres:postgres@localhost:5432/spacedb php src/migrate.php
# start the built-in development server
DATABASE_URL=postgres://postgres:postgres@localhost:5432/spacedb php -S localhost:8080 public/index.php
# or with individual variables
DB_HOST=localhost DB_USER=postgres DB_PASSWORD=secret php src/migrate.php
DB_HOST=localhost DB_USER=postgres DB_PASSWORD=secret php -S localhost:8080 public/index.php