Ship It
From localhost to production. Plus: when to use ZealPHP and when not to.
You will learn
- Run ZealPHP as a background daemon
- Set up Nginx as a reverse proxy (HTTP + WebSocket + SSE)
- Write a systemd service unit
- When ZealPHP is the right tool and when it isn't
The problem
Your app runs on localhost:8080. You close the terminal and it dies. You need it
on a real server, running 24/7, behind HTTPS, surviving reboots.
1. Daemon mode
php app.php start -p 8080 -d # daemonize
php app.php status # check if running
php app.php stop # stop
php app.php restart # cycle
The -d flag puts the server in the background. PID files live at
/tmp/zealphp/zealphp_{port}.pid. Logs default to /tmp/zealphp/.
2. Nginx reverse proxy
server {
listen 80;
server_name myapp.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# SSE — disable buffering
proxy_buffering off;
proxy_cache off;
}
}
proxy_buffering off is critical for SSE and streaming — without it, Nginx buffers the entire response before forwarding.
3. systemd service
This matches deploy/zealphp.service
in the repo — copy it as /etc/systemd/system/zealphp.service and adjust the two
paths.
[Unit]
Description=ZealPHP App Server
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/php app.php
# Graceful shutdown — SIGTERM then SIGKILL after 30s
ExecStop=/bin/kill -TERM $MAINPID
KillMode=mixed
TimeoutStopSec=30s
Restart=on-failure
RestartSec=2s
LimitNOFILE=65535
Environment=OPENAI_API_KEY=sk-...
# Captures stdout/stderr → journalctl -u zealphp -f
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
4. Docker
This is a teaching version showing the pieces inline. The
canonical Dockerfile
uses setup.sh --docker for the OpenSwoole / ext-zealphp install,
which keeps the same recipe in one script across CLI installs and Docker builds.
FROM php:8.3-cli-bookworm
RUN apt-get update && apt-get install -y libssl-dev libcurl4-openssl-dev git \
&& pecl install openswoole && docker-php-ext-enable openswoole \
&& git clone --depth 1 https://github.com/sibidharan/ext-zealphp.git /tmp/ext-zealphp \
&& cd /tmp/ext-zealphp && phpize && ./configure && make && make install && rm -rf /tmp/ext-zealphp \
&& docker-php-ext-enable zealphp
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader
COPY . .
EXPOSE 8080
CMD ["php", "app.php"]
The CMD is bare php app.php — no -d, no start -p
flags. Docker containers run in the foreground; daemonize flags would orphan the process and
Docker would exit immediately.
5. Environment variables
OPENAI_API_KEY # Real AI chat; mock mode without it
ZEALPHP_LEARN_AI_MODEL # Model name (default: gpt-4.1-mini)
ZEALPHP_LEARN_DB_PATH # SQLite path (default: storage/learn.db)
ZEALPHP_WORKERS # HTTP worker count (default: CPU cores)
ZEALPHP_PORT # Listen port (default: 8080)
ZEALPHP_LOG_DIR # Log directory (default: /tmp/zealphp)
What you built
Over 25 lessons you created a real application:
- Session-based auth with SQLite +
password_hash - CRUD notes app with htmx — no page reloads
- AI chat assistant that streams tool calls via SSE
- Cross-tab sync via WebSocket
- Agent-via-API — Python calls the same endpoints as the frontend
- Parallel I/O with
go() + Channel
All served from one php app.php process. No Redis. No Node sidecar.
No queue worker. One process, one language.
When ZealPHP is right
ZealPHP on 4 workers benchmarks at over 100,000 req/s with low single-digit ms p90 latency. For SaaS dashboards, content sites, internal tools, AI wrappers — it's more than enough. The bottleneck is almost always the database or external API, not the framework.
htmx covers 95% of interactivity needs with four HTML attributes. The remaining 5% (drag-and-drop, collaborative editing, client-side state) is where React earns its complexity.
The boring architecture
A React + Node + Redis + queue-worker stack has six moving parts. A ZealPHP app has one:
php app.php. Fewer moving parts means fewer things to monitor, fewer things
to break at 3am, fewer things to explain to the next developer.
The boring architecture is the one that ships and stays shipped.
Key Takeaways
php app.php start -ddaemonizes;systemdkeeps it alive- Nginx reverse proxy needs
proxy_buffering offfor SSE/streaming - One process handles HTTP, WebSocket, SSE, sessions, shared memory — no external services
- ZealPHP is right for most web apps; reach for React only when you need client-side state