Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.

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 -d daemonizes; systemd keeps it alive
  • Nginx reverse proxy needs proxy_buffering off for 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