🇬🇧 English — jump to the English version below ↓
Minimalistický deploy přes HTTP pro omezené hostingy.
HTTPDeploy je nouzové řešení nasazování pro levné nebo zamčené hostingy, které nenabízejí žádné SSH, žádný Git, žádné CI/CD a neumí se připojit na GitHub — typický sdílený hosting, kde je jediným reálným kanálem na server HTTP(S) a případně FTP.
Princip je jednoduchý:
- Na lokálním stroji spustíš
production.ps1(Windows) neboproduction.sh(Linux/macOS). Skript zabalí projekt dotar.gzaPOSTne ho na server přes HTTPS. - Na serveru jediný soubor —
deploy.php— balíček přijme, rozbalí ho do web rootu a (volitelně) spustí SQL migrace.
Žádní build agenti, žádné SSH klíče na serveru, žádná odchozí spojení ze serveru. Lokální stroj pushuje, server rozbaluje.
⚠️ Je to pragmatický workaround, ne náhrada za pořádnou CI/CD pipeline. Pokud tvůj hosting umí SSH + Git, použij raději to.
┌─────────────────────┐ HTTPS POST ┌──────────────────────┐
│ Lokální stroj │ package.tar.gz ───────▶ │ Server │
│ │ X-Deploy-Token: … │ │
│ production.ps1/sh │ migrate=1 │ deploy.php │
│ • git diff │ delete=… │ • ověř IP+token │
│ • zabal tar.gz │ │ • rozbal do rootu │
│ • curl upload │ ◀─────── textová odpověď │ • spusť migrace │
└─────────────────────┘ └──────────────────────┘
- Klient po serveru nikdy nechce, aby si něco stahoval — pošle mu bajty.
- Server ověří požadavek přes IP allow-list (volitelně) a sdílený token,
pak rozbalí archiv do web rootu. K rozbalení používá
zlib(gzopen) s malým vestavěným čtečem tar formátu a jako zálohuPhar— funguje tedy i na hostingu, kde je rozšířenípharvypnuté. - Běhová data jsou chráněná:
config.php,.deploy-token, VCS složky a cokoliv uvedeš vDEPLOY_PROTECTEDse nikdy nepřepíše ani nesmaže.
| Soubor | Kde běží | K čemu slouží |
|---|---|---|
deploy.php |
Server | Přijme balíček, rozbalí ho, spustí migrace |
sql/migrate.php |
Server | Volitelný runner DB migrací (volá ho deploy.php) |
production.ps1 |
Lokál (Win) | Zabalí a nahraje projekt |
production.sh |
Lokál (*nix) | Zabalí a nahraje projekt |
config.sample.php |
obojí | Zkopíruj na config.php a vyplň |
.deployignore |
Lokál | Volitelně: další cesty vyloučené z uploadu |
maintenance.html |
Server | Volitelně: stránka „hned jsme zpět" během nasazení |
.deploy-lock |
Server | Semafor — deploy.php ho vytvoří po dobu nasazení |
Zkopíruj deploy.php, složku sql/, config.sample.php a skripty
production.* do kořene svého webového projektu (do složky, která na serveru
odpovídá web rootu).
cp config.sample.php config.phpUprav config.php:
DEPLOY_TOKEN— dlouhý náhodný sdílený tajný klíč. Vygeneruj ho přes:Můžeš ho nechat přímo zde, nebo dát do souboruphp -r "echo bin2hex(random_bytes(32)), PHP_EOL;".deploy-tokenvedleconfig.php(git-ignored) — pokud existuje, má přednost.DEPLOY_ALLOWED_IPS— ⭐ tvoje nejsilnější ochrana, nastav ji, kdykoliv to jde. Seznam povolených IP / CIDR. Endpoint umožní vzdálené spuštění kódu komukoliv, kdo má token, a není zde žádný rate-limit ani lockout — únik nebo uhodnutí tokenu znamená kompromitaci serveru. Pokud máš statickou IP (server, kancelář, VPN, CI runner), uveď ji sem. Prázdný seznam = povolena jakákoliv IP (jen token, nejméně bezpečné) — pak nasaď dlouhý náhodný token a měj zapnuté HTTPS.DEPLOY_PROTECTED— relativní cesty (uploady, logy, cache…), které deploy nikdy nesmí přepsat ani smazat.DEPLOY_ALLOW_HTTP— ve výchozím stavu endpoint odmítá nešifrované HTTP (token by jinak šel po síti v plaintextu). Nastav natruejen pro důvěryhodnou síť / lokální testy.DEPLOY_MAX_PACKAGE_MB/DEPLOY_MAX_UNPACKED_MB— stropy proti přílišnému nebo škodlivě komprimovanému balíčku („gzip bomba"). Výchozí 100 MB / 1024 MB; zvyš, pokud je projekt větší.DB_*— potřeba jen pokud používáš migrace.
Stejný config.php musí existovat i na serveru (s vlastními DB přihlašovacími
údaji serveru). Je git-ignored a deploy ho nikdy nepřepíše, takže si každé
prostředí drží vlastní kopii.
Předej -Url / --url při každém spuštění, nebo vytvoř v kořeni projektu soubor
.deploy-url s endpointem:
https://example.com/deploy.php
deploy.php zapisuje soubory přímo do své vlastní složky, takže PHP proces tam
musí mít právo zápisu. K rozbalení balíčku potřebuje buď zlib (gzopen,
prakticky v každém PHP buildu) nebo rozšíření Phar — stačí jedno z nich.
# Windows
.\production.ps1# Linux / macOS
./production.shZabalí celý projekt (bez vyloučených cest), nahraje ho a spustí migrace. Plné nasazení na serveru nikdy nic nemaže.
./production.sh --changed # soubory změněné posledním commitem
./production.sh --changed --since HEAD~2 # změny za POSLEDNÍ 2 commity (HEAD~2..HEAD)
./production.sh --changed --since v1.2 # změny od tagu v1.2 po HEAD
./production.sh --changed --since abc1234 # změny od konkrétního commitu po HEAD--changed (PowerShell: -Changed) nasadí jen soubory dotčené posledním
commitem a smaže na serveru soubory, které ten commit odstranil. Posílá obsah
pracovního stromu, takže se nasazuje to, co máš na disku.
Potřebuješ nasadit víc než poslední commit? Použij --since <ref> (PowerShell:
-Since <ref>) — rozsah je vždy <ref>..HEAD. Jako <ref> projde cokoli, co
git umí přeložit:
- N commitů zpět →
HEAD~N(např.--since HEAD~2= poslední 2 commity), - tag →
v1.2, - konkrétní commit → jeho SHA (
abc1234), - větev →
main(změny od posledního společného stavu po HEAD).
Tip: nejdřív si rozsah ověř přes --dry-run (PowerShell: -DryRun),
než cokoli odešleš.
./production.sh --changed --dry-run # vypíše, co by se poslalo/smazalo./production.sh --no-migrate # PowerShell: -NoMigrate| PowerShell | bash | Význam |
|---|---|---|
-Url |
--url |
Deploy endpoint (přebije .deploy-url) |
-Token |
--token |
Deploy token (přebije .deploy-token) |
-Changed |
--changed |
Nasadí jen soubory změněné posledním commitem |
-Since <ref> |
--since <ref> |
S -Changed: rozsah <ref>..HEAD |
-NoMigrate |
--no-migrate |
Přeskočí migrace |
-NoGitIgnore |
--no-gitignore |
Nerespektovat .gitignore (nahraje i ignorované) |
-DryRun |
--dry-run |
Jen vypíše, nic nenahraje |
Klient nikdy nenahrává vlastní nástroje (production.*, config.php,
config.sample.php, README.md), VCS složky (.git, .github, .svn) ani
tajné soubory (.deploy-token, .deploy-url).
.gitignore se respektuje automaticky. Pokud je projekt git repozitář, vyloučí
se z uploadu vše, co git ignoruje (typicky config.php/tajné soubory, logy,
uploads, cache, build výstup) — ignorovaný soubor se tak nikdy nenahraje ani
nepřepíše svůj produkční protějšek. Vypneš to přepínačem -NoGitIgnore /
--no-gitignore.
⚠️ Pozor na neukotvená pravidla v.gitignore. Vzorlog/(bez úvodního lomítka) odpovídá složcelogv jakékoli hloubce — na Windows i bez ohledu na velikost písmen — takže může nechtěně chytit třebavendor/.../Log/a vyhodit ji z deploye. Pro jen kořenovou složku piš/log/. Ověř sigit ls-files --others --ignored --exclude-standard --directory, co se vyloučí.
Pro výjimky specifické pro projekt (mimo .gitignore, nebo když repo .gitignore
nemá) přidej do kořene soubor .deployignore — jedna cesta nebo prefix složky
na řádek (# uvozuje komentář):
# .deployignore
node_modules
tests
storage/cache
*.map
uploads
Na straně serveru je autoritativní pojistkou
DEPLOY_PROTECTED— i kdyby se soubor do balíčku dostal, uvedené cesty se nikdy nepřepíšou ani nesmažou.
Po dobu, kdy deploy.php přepisuje soubory ve web rootu, je tam přítomný
semafor — soubor .deploy-lock. Vznikne těsně předtím, než se začne sahat
na web root, a zase zmizí, jakmile je hotovo (sync + mazání + migrace). I kdyby
nasazení v půlce spadlo nebo vypršel časový limit, deploy.php semafor uklidí
přes shutdown handler. Soubor je chráněný (DEPLOY_PROTECTED), takže ho samotné
nasazení nikdy nepřepíše ani nesmaže.
Tvůj web se na semafor může podívat a po tu (obvykle podsekundovou) chvíli
servírovat stránku „hned jsme zpět" — viz přiložený maintenance.html.
PHP — na začátku front controlleru (index.php):
$lock = __DIR__ . '/.deploy-lock';
// Pojistka proti uvíznutí: starší zámek než 5 minut ignoruj.
if (is_file($lock) && time() - filemtime($lock) < 300) {
http_response_code(503);
header('Retry-After: 30');
header('Cache-Control: no-store');
readfile(__DIR__ . '/maintenance.html');
exit;
}IIS / web.config (čistě statický web bez PHP front controlleru) — pravidlo,
které při existenci zámku přesměruje vše na maintenance.html. IIS test
{APPL_PHYSICAL_PATH}.deploy-lock ověří přítomnost souboru:
<rule name="MaintenanceLock" stopProcessing="true">
<match url=".*" />
<conditions>
<add input="{APPL_PHYSICAL_PATH}.deploy-lock" matchType="IsFile" />
<add input="{REQUEST_FILENAME}" pattern="maintenance\.html$" negate="true" />
</conditions>
<action type="Rewrite" url="/maintenance.html" />
</rule>Apache / .htaccess (vyžaduje mod_rewrite, příp. mod_headers) — pokud
zámek existuje, vrať 503 a jako tělo pošli maintenance.html. Díky
ErrorDocument zůstane stavový kód 503 (na rozdíl od R=503,L se prohlížeči
opravdu pošle obsah stránky):
RewriteEngine On
# Když existuje semafor a nejde už o samotnou maintenance stránku → 503.
RewriteCond %{DOCUMENT_ROOT}/.deploy-lock -f
RewriteCond %{REQUEST_URI} !=/maintenance.html
RewriteRule ^ - [R=503,L]
ErrorDocument 503 /maintenance.html
# Retry-After přidej jen po dobu zámku (mod_headers + Apache 2.4):
<If "-f '%{DOCUMENT_ROOT}/.deploy-lock'">
Header always set Retry-After "30"
</If>
maintenance.htmlje jeden soběstačný soubor (žádné externí CSS/JS/obrázky), aby fungoval i uprostřed nasazení, kdy mohou ostatní assety chvíli chybět. Vrací se s503+Retry-After, takže to vyhledávače chápou jako dočasný stav.
Pokud na serveru existuje sql/migrate.php, deploy.php ho po rozbalení spustí
(pokud nebylo použito --no-migrate). Pokud neexistuje, deploy migrace prostě
přeskočí — jsou zcela volitelné.
Runner je založený na konvenci a žije ve složce sql/:
sql/schema.sql— kompletní počáteční schéma. Aplikuje se jednou na čerstvou databázi jako verze 0.sql/migrate_v1.sql,sql/migrate_v2.sql, … — inkrementální změny, aplikované vzestupně podleN, každá zaznamenaná po úspěšném provedení.
Při prvním spuštění:
- Vytvoří databázi pojmenovanou v
config.php, pokud neexistuje. - Vytvoří evidenční tabulku
migrations. - Aplikuje
schema.sql(pokud existuje), pak všechny nečekanémigrate_v*.sql.
Příkazy se dělí podle ; a spouštějí jeden po druhém. Běžné chyby „už existuje"
(1050, 1060, 1061, 1062, 1091) se tolerují, takže opakované spuštění je
bezpečné.
Můžeš ho spustit i ručně:
php sql/migrate.phpPokud sql/ neobsahuje schema.sql ani žádné migrate_v*.sql, runner neudělá
nic a databázi nechá nedotčenou.
- HTTPS je povinné. Token putuje v hlavičce a endpoint ve výchozím stavu
odmítá HTTP (lze vypnout přes
DEPLOY_ALLOW_HTTP, nedoporučeno). - ⭐ Omez podle IP, kdykoliv to jde (
DEPLOY_ALLOWED_IPS) — to je nejsilnější ochrana. Není žádný rate-limit ani lockout, takže s prázdným seznamem je token jediná bariéra mezi internetem a tvým web rootem. Omezení na pevnou IP (server / kancelář / VPN / CI runner) sníží plochu útoku řádově. - Token ber jako heslo. Posílá se jen v hlavičce
X-Deploy-Token(ne jako POST pole). Použij dlouhý náhodný (random_bytes(32)); při úniku ihned rotuj. - Drž
deploy.phpza tokenem. Kdokoliv, kdo umí poslat platný token, ti může přepsat web root. config.phpa.deploy-tokenjsou git-ignored — drž je mimo repozitář.- Na prostředích, kam nenasazuješ, zvaž odstranění/přejmenování
deploy.php.
- Server: PHP 7.4+ se
zlibneboPhar(na rozbalení),PDO/pdo_mysql(jen pokud používáš migrace) a zapisovatelný web root. - Lokál:
git,curlatarvPATH. PowerShell 5+ na Windows nebo Bash 4+ na Linux/macOS.
Samostatné end-to-end testy jsou ve složce test/ — deploy-test.ps1 testuje
production.ps1, deploy-test.sh testuje production.sh. Každý spustí dočasný
PHP server hostující deploy.php, nasadí na něj vzorový projekt a ověří, že
soubory dorazí, chráněné cesty přežijí, migrace proběhnou a mazání v changed
režimu funguje. Potřebují PHP, git, tar, curl a lokální MariaDB/MySQL:
# PowerShell klient
.\test\deploy-test.ps1 # php z PATH, DB uživatel root, prázdné heslo
.\test\deploy-test.ps1 -Php c:\php\php.exe -DbUser root -DbPass secret -Port 8123# bash klient (proměnné prostředí: PHP, PORT, DBHOST, DBUSER, DBPASS)
PHP=php DBUSER=root DBPASS=secret PORT=8101 ./test/deploy-test.shRadek Hulán — https://mywebdesign.cz/
MIT. Použij to, uprav, nasaď.
🇨🇿 Česky — skok na českou verzi nahoře ↑
A minimal HTTP-based deploy tool for restricted hosting.
HTTPDeploy is a last-resort deployment solution for cheap or locked-down hosting that offers no SSH, no Git, no CI/CD and cannot reach out to GitHub — the kind of shared hosting where your only real channel to the server is HTTP(S) and maybe FTP.
The idea is simple:
- On your local machine you run
production.ps1(Windows) orproduction.sh(Linux/macOS). It packs your project into atar.gzandPOSTs it to the server over HTTPS. - On the server, a single file —
deploy.php— receives the package, unpacks it into the web root, and (optionally) runs your SQL migrations.
No build agents, no SSH keys on the host, no outbound connections from the server. Your local machine pushes; the server unpacks.
⚠️ This is a pragmatic workaround, not a replacement for a proper CI/CD pipeline. If your hosting supports SSH + Git, use that instead.
┌─────────────────────┐ HTTPS POST ┌──────────────────────┐
│ Local machine │ package.tar.gz ───────▶ │ Server │
│ │ X-Deploy-Token: … │ │
│ production.ps1/sh │ migrate=1 │ deploy.php │
│ • git diff │ delete=… │ • verify IP+token │
│ • pack tar.gz │ │ • unpack to root │
│ • curl upload │ ◀─────── plain-text resp │ • run migrations │
└─────────────────────┘ └──────────────────────┘
- The client never trusts the server to fetch anything — it sends the bytes.
- The server authenticates the request by IP allow-list (optional) and a
shared token, then unpacks the archive into the web root. Unpacking uses
zlib(gzopen) with a small built-in tar reader, and falls back toPharifzlibis unavailable — so it works even on hosting where thepharextension is disabled. - Runtime data is protected:
config.php,.deploy-token, the VCS folders and anything you list inDEPLOY_PROTECTEDare never overwritten or deleted.
| File | Where it runs | Purpose |
|---|---|---|
deploy.php |
Server | Receives the package, unpacks it, triggers migrations |
sql/migrate.php |
Server | Optional DB migration runner (called by deploy.php) |
production.ps1 |
Local (Win) | Packs and uploads the project |
production.sh |
Local (*nix) | Packs and uploads the project |
config.sample.php |
both | Copy to config.php and fill in |
.deployignore |
Local | Optional: extra paths to exclude from uploads |
maintenance.html |
Server | Optional: "be right back" page shown during a deploy |
.deploy-lock |
Server | Semaphore — deploy.php raises it during a deploy |
Copy deploy.php, the sql/ folder, config.sample.php and the
production.* scripts into the root of your web project (the folder that maps
to your web root on the server).
cp config.sample.php config.phpEdit config.php:
DEPLOY_TOKEN— a long random shared secret. Generate one with:You can keep it inline, or put it in aphp -r "echo bin2hex(random_bytes(32)), PHP_EOL;".deploy-tokenfile next toconfig.php(git-ignored) — if present, that file wins.DEPLOY_ALLOWED_IPS— ⭐ your strongest control; set it whenever you can. IP / CIDR allow-list. The endpoint grants remote code execution to anyone holding the token, and there is no rate limit or lockout — a leaked or guessed token means a compromised server. If you have a static IP (server, office, VPN, CI runner), list it here. An empty list allows any IP (token-only, least safe) — then use a long random token and keep HTTPS on.DEPLOY_PROTECTED— relative paths (uploads, logs, caches…) that a deploy must never overwrite or delete.DEPLOY_ALLOW_HTTP— the endpoint refuses plain HTTP by default (the token would otherwise cross the network in cleartext). Set totrueonly for trusted-network / local testing.DEPLOY_MAX_PACKAGE_MB/DEPLOY_MAX_UNPACKED_MB— caps against an oversized or maliciously compressed package (a "gzip bomb"). Defaults 100 MB / 1024 MB; raise them if your project is genuinely larger.DB_*— only needed if you use migrations.
The same config.php must exist on the server too (with the server's own
DB credentials). It is git-ignored and never overwritten by a deploy, so each
environment keeps its own copy.
Pass -Url / --url each time, or create a .deploy-url file in the project
root containing the endpoint:
https://example.com/deploy.php
deploy.php writes files directly into its own directory, so the PHP process
must have write permission there. To unpack the package it needs either
zlib (gzopen, present on virtually every PHP build) or the Phar
extension — one of the two is enough.
# Windows
.\production.ps1# Linux / macOS
./production.shPacks the whole project (minus excluded paths), uploads it, and runs migrations. A full deploy never deletes anything on the server.
./production.sh --changed # files changed by the last commit
./production.sh --changed --since HEAD~2 # changes over the LAST 2 commits (HEAD~2..HEAD)
./production.sh --changed --since v1.2 # changes since tag v1.2 up to HEAD
./production.sh --changed --since abc1234 # changes since a specific commit up to HEAD--changed (PowerShell: -Changed) deploys only the files touched by the last
commit, and deletes on the server the files that commit removed. It sends
the working-tree content, so what's on disk is what's deployed.
Need more than the last commit? Use --since <ref> (PowerShell: -Since <ref>)
— the range is always <ref>..HEAD. The <ref> can be anything git can resolve:
- N commits back →
HEAD~N(e.g.--since HEAD~2= last 2 commits), - a tag →
v1.2, - a specific commit → its SHA (
abc1234), - a branch →
main(changes since the merge base up to HEAD).
Tip: confirm the range with --dry-run (PowerShell: -DryRun) before sending.
./production.sh --changed --dry-run # print what would be sent/deleted./production.sh --no-migrate # PowerShell: -NoMigrate| PowerShell | bash | Meaning |
|---|---|---|
-Url |
--url |
Deploy endpoint (overrides .deploy-url) |
-Token |
--token |
Deploy token (overrides .deploy-token) |
-Changed |
--changed |
Deploy only files changed by the last commit |
-Since <ref> |
--since <ref> |
With -Changed: range <ref>..HEAD |
-NoMigrate |
--no-migrate |
Skip running migrations |
-NoGitIgnore |
--no-gitignore |
Do not honor .gitignore (upload ignored files) |
-DryRun |
--dry-run |
Print only, upload nothing |
The client never uploads its own tooling (production.*, config.php,
config.sample.php, README.md), the VCS folders (.git, .github, .svn)
or the secret files (.deploy-token, .deploy-url).
.gitignore is honored automatically. In a git work tree, everything git
ignores (typically config.php/secrets, logs, uploads, caches, build output)
is excluded from the upload too — so an ignored file is never shipped and can
never overwrite its production counterpart. Turn it off with -NoGitIgnore /
--no-gitignore.
⚠️ Beware unanchored.gitignorerules. A pattern likelog/(no leading slash) matches alogdirectory at any depth — and on Windows regardless of case — so it can accidentally catch e.g.vendor/.../Log/and drop it from the deploy. Anchor it as/log/to mean the root folder only. Check what will be excluded withgit ls-files --others --ignored --exclude-standard --directory.
For project-specific exclusions (outside .gitignore, or when the repo has no
.gitignore), add a .deployignore file to the root — one path or directory
prefix per line (# starts a comment):
# .deployignore
node_modules
tests
storage/cache
*.map
uploads
Server-side,
DEPLOY_PROTECTEDis the authoritative guard — even if a file slips into the package, the listed paths are never overwritten or deleted.
While deploy.php is replacing files in the web root, a semaphore file —
.deploy-lock — is present there. It is raised right before the web root is
touched and removed once everything is done (sync + delete + migrations). Even if
the deploy crashes halfway or times out, deploy.php clears it via a shutdown
handler. The file is protected (DEPLOY_PROTECTED), so the deploy itself never
overwrites or deletes it.
Your site can check for the semaphore and serve a "be right back" page during
that (usually sub-second) window — see the bundled maintenance.html.
PHP — at the top of your front controller (index.php):
$lock = __DIR__ . '/.deploy-lock';
// Safety valve against a stuck lock: ignore one older than 5 minutes.
if (is_file($lock) && time() - filemtime($lock) < 300) {
http_response_code(503);
header('Retry-After: 30');
header('Cache-Control: no-store');
readfile(__DIR__ . '/maintenance.html');
exit;
}IIS / web.config (purely static site, no PHP front controller) — a rule
that rewrites everything to maintenance.html while the lock exists. The
{APPL_PHYSICAL_PATH}.deploy-lock test checks the file's presence:
<rule name="MaintenanceLock" stopProcessing="true">
<match url=".*" />
<conditions>
<add input="{APPL_PHYSICAL_PATH}.deploy-lock" matchType="IsFile" />
<add input="{REQUEST_FILENAME}" pattern="maintenance\.html$" negate="true" />
</conditions>
<action type="Rewrite" url="/maintenance.html" />
</rule>Apache / .htaccess (needs mod_rewrite, plus mod_headers for the header)
— while the lock exists, return 503 and serve maintenance.html as the body.
Using ErrorDocument keeps the 503 status code (unlike R=503,L alone, the
page body is actually sent to the browser):
RewriteEngine On
# Lock present and this isn't the maintenance page itself → 503.
RewriteCond %{DOCUMENT_ROOT}/.deploy-lock -f
RewriteCond %{REQUEST_URI} !=/maintenance.html
RewriteRule ^ - [R=503,L]
ErrorDocument 503 /maintenance.html
# Send Retry-After only while the lock is up (mod_headers + Apache 2.4):
<If "-f '%{DOCUMENT_ROOT}/.deploy-lock'">
Header always set Retry-After "30"
</If>
maintenance.htmlis a single self-contained file (no external CSS/JS/images) so it works even mid-deploy, when other assets may briefly be missing. It is returned with503+Retry-After, so search engines treat it as temporary.
If a sql/migrate.php exists on the server, deploy.php runs it after
unpacking (unless --no-migrate was used). If it doesn't exist, the deploy
simply skips migrations — they are entirely optional.
The runner is convention-based and lives in sql/:
sql/schema.sql— the full initial schema. Applied once on a fresh database as version 0.sql/migrate_v1.sql,sql/migrate_v2.sql, … — incremental changes, applied in ascending order ofN, each recorded once it succeeds.
On the first run it will:
- Create the database named in
config.phpif it does not exist. - Create a
migrationsbookkeeping table. - Apply
schema.sql(if present), then any pendingmigrate_v*.sql.
Statements are split on ; and run one by one. Common "already exists" errors
(1050, 1060, 1061, 1062, 1091) are tolerated, so re-running is safe.
You can also run it by hand:
php sql/migrate.phpIf sql/ contains no schema.sql and no migrate_v*.sql, the runner does
nothing and leaves the database untouched.
- HTTPS is mandatory. The token travels in a header and the endpoint
refuses plain HTTP by default (override with
DEPLOY_ALLOW_HTTP, not recommended). - ⭐ Restrict by IP whenever you can (
DEPLOY_ALLOWED_IPS) — it's the strongest control. There is no rate limit or lockout, so with an empty list the token is the only barrier between the internet and your web root. Pinning a fixed IP (server / office / VPN / CI runner) cuts the attack surface dramatically. - Treat the token like a password. It is sent only in the
X-Deploy-Tokenheader (not as a POST field). Use a long random one (random_bytes(32)) and rotate it immediately if leaked. - Keep
deploy.phpbehind the token. Anyone who can POST a valid token can overwrite your web root. config.phpand.deploy-tokenare git-ignored — keep them out of your repo.- Consider removing/renaming
deploy.phpon environments where you don't deploy.
- Server: PHP 7.4+ with
zliborPhar(to unpack),PDO/pdo_mysql(only if you use migrations), and a writable web root. - Local:
git,curlandtaronPATH. PowerShell 5+ on Windows or Bash 4+ on Linux/macOS.
Self-contained end-to-end tests live in test/ — deploy-test.ps1 exercises
production.ps1, deploy-test.sh exercises production.sh. Each starts a
throwaway PHP server hosting deploy.php, deploys a sample project onto it, and
asserts files land, protected paths survive, migrations run and changed-mode
deletes work. They need PHP, git, tar, curl and a local MariaDB/MySQL:
# PowerShell client
.\test\deploy-test.ps1 # php on PATH, DB user root, empty password
.\test\deploy-test.ps1 -Php c:\php\php.exe -DbUser root -DbPass secret -Port 8123# bash client (env vars: PHP, PORT, DBHOST, DBUSER, DBPASS)
PHP=php DBUSER=root DBPASS=secret PORT=8101 ./test/deploy-test.shRadek Hulán — https://mywebdesign.cz/
MIT. Use it, adapt it, ship it.