I Built a Public Docker Image for Laravel + Swoole with io_uring Support (Here's Why It Exists)
Direct answer: The official phpswoole/swoole Docker image does not compile io_uring support — a Linux kernel async I/O mechanism that meaningfully reduces syscall overhead for high-concurrency Swoole servers. I built and published roshankc1234/optimized-laravel-runtime to fix that: PHP 8.5 + Swoole 6.2.2 rebuilt from source with --enable-iouring, Nginx, Supervisor, and common PHP extensions — in a lean ~78MB Alpine image anyone can use as a Laravel Octane base.
The image is multi-arch: it supports both linux/amd64 and linux/arm64. If you have a choice, run it on ARM64 (AWS Graviton, Apple Silicon in CI, Ampere Altra). io_uring on ARM64 delivers excellent throughput, and Graviton3 instances typically offer 20–40% better price/performance than equivalent x86 EC2 instances — pairing io_uring with arm64 is the highest-leverage configuration for most Laravel API workloads.
This post explains what io_uring is, why ARM64 is the preferred target, what's inside the image, and how to wire it into your Laravel app in 15 minutes.
What io_uring actually is
io_uring is a Linux kernel interface for asynchronous I/O introduced by Jens Axboe in kernel 5.1 (2019) and substantially improved in 5.6 and 5.10. Before io_uring, the dominant async I/O mechanism in high-performance servers was epoll — an event notification mechanism that tells you when a file descriptor is ready for I/O, but still requires separate read()/write() syscalls to actually perform the operation.
io_uring fundamentally changes the model with two ring buffers shared between userspace and the kernel — a submission queue (SQ) and a completion queue (CQ). Your program drops I/O requests into the SQ; the kernel processes them and places results in the CQ; your program reads completions. The key advantage: multiple I/O operations can be submitted and completed with a single syscall, or even with zero syscalls when the kernel can process completions without being interrupted.
For a Swoole HTTP server handling thousands of concurrent connections, the numbers matter:
| Mechanism | I/O model | Syscall overhead | Kernel-userspace transitions |
|---|---|---|---|
| epoll (classic) | Notify-then-read: two phases per operation | One syscall per event wait + one per read/write | 2 per I/O operation minimum |
| io_uring (this image) | Submit-and-complete via ring buffers | Single io_uring_enter for N operations; zero-syscall in SQPOLL mode |
Near-zero in SQPOLL; fraction of epoll under load |
Real-world benchmarks on io_uring vs epoll for web server workloads (from the original Axboe paper and subsequent benchmarks) show throughput improvements in the range of 10–50% for I/O-intensive workloads at high concurrency, with larger gains appearing as connection counts increase. Cloudflare has published data showing io_uring-based servers outperforming epoll-based equivalents by 15–30% on their edge workloads. The gains are workload-dependent — a PHP app spending most of its time in the database will see less improvement than a file-serving or network-proxying workload.
Why ARM64 is the preferred architecture for this image
Both amd64 and arm64 are supported and tested. But if you're deploying on AWS and haven't already switched to Graviton, now is a good time to consider it — and io_uring makes the case even stronger.
Here's the combined argument for ARM64 + io_uring:
| Factor | amd64 (x86_64) | arm64 (Graviton / Ampere) |
|---|---|---|
| io_uring support | Full (kernel ≥ 5.12) | Full (kernel ≥ 5.12, AL2023 ships 6.1) |
| Price/performance vs equivalent x86 | Baseline | ~20–40% better (AWS Graviton3 vs Intel/AMD EC2) |
| Throughput/watt | Good | Significantly better — relevant for sustained high-concurrency Swoole workloads |
| Docker multi-arch image | ✅ Included in manifest | ✅ Included in manifest (also available as dedicated arm64 tag) |
| Swoole 6.2.2 build | Compiled and tested | Compiled and tested; io_uring verified on AL2023 arm64 |
| Recommended for production | Yes — especially if you're already on x86 | Preferred — best throughput per dollar with io_uring |
AWS Graviton3 (C7g, M7g, R7g families) runs Amazon Linux 2023 with a 6.1 kernel by default. Pull the multi-arch image and Docker will automatically select the arm64 layer on Graviton. You don't need to specify the platform flag unless you're cross-compiling locally on an x86 machine:
# On Graviton: Docker automatically picks arm64 layer
docker pull roshankc1234/optimized-laravel-runtime:php8.5-swoole6.2.2-iouring
# Force arm64 explicitly (e.g. cross-compile on x86 CI):
docker pull --platform linux/arm64 roshankc1234/optimized-laravel-runtime:php8.5-swoole6.2.2-iouring
# Dedicated arm64-only tag (same image, no manifest lookup overhead):
docker pull roshankc1234/optimized-laravel-runtime:php8.5-swoole6.2.2-iouring-arm64
Why the official phpswoole image doesn't have io_uring
The official phpswoole/swoole images compile Swoole with a standard set of flags. As of Swoole 6.x, --enable-iouring is an opt-in compile flag — it's not included in the standard build because:
- io_uring requires Linux kernel ≥ 5.1 (the official image targets wider compatibility, including older kernels)
- Full io_uring functionality (particularly SQPOLL mode) requires
CAP_SYS_NICEor similar kernel permissions that not all Docker environments grant by default - The seccomp profile Docker applies by default blocks some
io_uring_*syscalls, which would cause silent fallback or errors for users who don't know to configure it
These are all legitimate reasons not to include it by default. But if you're running on a modern Linux host (kernel 5.12+, which covers Amazon Linux 2023, Ubuntu 22.04+, and most current cloud images), you're leaving performance on the table by not using io_uring.
What's inside the image
The image is built in two stages on an Alpine base:
- Build stage: Full compiler toolchain (gcc, g++, make, autoconf) + Swoole source code compiled with
--enable-iouringand other performance flags. - Runtime stage: Only the compiled Swoole extension and its runtime dependencies. No compilers, no build tools. This is what keeps the image lean.
| Component | Version / detail |
|---|---|
| PHP | 8.5 (Alpine) |
| Swoole | 6.2.2, compiled with --enable-iouring |
| Web server | Nginx (reverse proxy in front of Swoole) |
| Process manager | Supervisor (manages Nginx + Octane workers) |
| PHP extensions | pdo_pgsql, redis, gd (jpeg/png/webp/avif), zip, exif, pcntl |
| PHP memory limit | 512M |
| PHP upload/POST limit | 100M |
| max_execution_time | 300s |
| Architectures | linux/amd64 and linux/arm64 (multi-arch manifest) |
| Compressed image size | ~78MB |
What's not in the image: Composer, git, or application code. This is a base runtime — you copy your application on top of it. That's intentional: keeping build-time tools out of the runtime image is a security and size best practice.
Quick start: pull and verify
The multi-arch manifest means docker pull automatically picks the right layer for your host:
# Works on both x86 and Graviton/ARM — Docker selects the right layer
docker pull roshankc1234/optimized-laravel-runtime:php8.5-swoole6.2.2-iouring
# Verify io_uring compiled correctly
docker run --rm roshankc1234/optimized-laravel-runtime:php8.5-swoole6.2.2-iouring \
php --ri swoole | grep -i uring
You should see:
io_uring => enabled
Also check which architecture Docker selected on your host:
docker run --rm roshankc1234/optimized-laravel-runtime:php8.5-swoole6.2.2-iouring \
uname -m
# aarch64 → you are on arm64 (Graviton, M-series, Ampere)
# x86_64 → you are on amd64
If instead you see Create io_uring failed in Swoole logs at runtime, the image compiled correctly but Docker's seccomp profile is blocking the io_uring_* syscalls. Two ways to fix it:
# Option 1: Disable seccomp (development/trusted environments only)
docker run --security-opt seccomp=unconfined ...
# Option 2: Allow only io_uring syscalls (production-appropriate)
# Add to your docker-compose.yml:
security_opt:
- seccomp=./seccomp-iouring.json
A minimal seccomp profile that adds io_uring syscalls to Docker's default allowlist is straightforward to create — I'll link a reference profile below. The key syscalls to allow are io_uring_setup, io_uring_enter, and io_uring_register.
Dockerfile for your Laravel app
Here's a production-ready Dockerfile that uses the image as a base:
FROM roshankc1234/optimized-laravel-runtime:php8.5-swoole6.2.2-iouring
WORKDIR /var/www
# Copy application code
COPY --chown=www-data:www-data . .
# Install PHP dependencies (Composer must be available in your build stage)
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Optimize Laravel for production
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
# Copy Nginx and Supervisord config
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
A multi-stage version (recommended for production to keep Composer out of the final image):
# Stage 1: Install dependencies
FROM composer:2 AS deps
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Stage 2: Runtime
FROM roshankc1234/optimized-laravel-runtime:php8.5-swoole6.2.2-iouring
WORKDIR /var/www
COPY --chown=www-data:www-data . .
COPY --from=deps /app/vendor ./vendor
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
Nginx and Supervisord configuration
Nginx proxies incoming HTTP traffic to Octane's Swoole server running on port 8000 internally. The key Nginx block:
server {
listen 80;
server_name _;
root /var/www/public;
index index.php;
location / {
proxy_pass http://127.0.0.1:8000;
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;
proxy_connect_timeout 60s;
proxy_read_timeout 300s;
}
}
Supervisord manages both Nginx and the Octane worker process, restarting either if they die:
[supervisord]
nodaemon=true
logfile=/dev/stdout
logfile_maxbytes=0
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:octane]
command=php /var/www/artisan octane:start --server=swoole --host=0.0.0.0 --port=8000 --workers=4
directory=/var/www
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Tune --workers to your CPU count. A common starting point is 2 × CPU cores for I/O-bound Laravel apps. Use --task-workers separately if you want dedicated task workers.
docker-compose.yml for local development
version: "3.9"
services:
app:
build: .
ports:
- "80:80"
environment:
APP_ENV: local
APP_DEBUG: "true"
security_opt:
- seccomp=./docker/seccomp-iouring.json
volumes:
- .:/var/www
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: laravel
POSTGRES_USER: laravel
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
pgdata:
Kernel requirements and cloud compatibility
io_uring requires Linux kernel ≥ 5.1. The SQPOLL feature (zero-syscall mode) requires ≥ 5.11. For production, I recommend ≥ 5.12. Here's what that means for common cloud environments:
| Platform / OS | Typical kernel | io_uring compatible? |
|---|---|---|
| Amazon Linux 2023 | 6.1 | ✅ Yes (recommended) |
| Ubuntu 22.04 LTS | 5.15 | ✅ Yes |
| Ubuntu 24.04 LTS | 6.8 | ✅ Yes |
| Debian 12 (Bookworm) | 6.1 | ✅ Yes |
| Amazon Linux 2 | 4.14 / 5.10 | ⚠️ 5.10 builds work; SQPOLL limited |
| macOS (Docker Desktop) | Linux VM (varies) | ⚠️ May need seccomp=unconfined for dev |
| Windows WSL2 | 5.15+ (recent builds) | ⚠️ Works but seccomp config needed |
Check your kernel version with uname -r. If you're on a kernel that supports io_uring but Swoole logs a failure, check sysctl kernel.io_uring_disabled — a value of 2 means io_uring is disabled system-wide by the host. Values 0 (enabled) or 1 (requires a privilege) are fine for Docker workloads.
What's not included and why
The image intentionally excludes Composer, git, and application source code. The reasoning:
- Security surface. Composer and git are build tools; they have no business being in a production runtime image. If an attacker gains code execution in the container, having a package manager available makes lateral movement significantly easier.
- Image size. Excluding build tools keeps the compressed image around 78MB. A comparable image with Composer, git, and build tooling is typically 200–400MB compressed.
- Layer caching. Multi-stage builds let you cache dependency installation separately from application code. A code change in your app doesn't invalidate the vendor directory layer.
When this image is the right choice
Use this image when:
- You're running Laravel Octane with the Swoole server driver.
- Your host runs Linux kernel ≥ 5.12 (most modern cloud environments do).
- You want io_uring I/O optimization without compiling Swoole yourself.
- You want an Alpine-based runtime with Nginx and Supervisor already wired up.
- Ideal: You're deploying to AWS Graviton (arm64) — io_uring + arm64 gives you the best throughput per dollar of any configuration this image supports.
Skip this image if:
- You're running Octane with RoadRunner (different server driver entirely).
- Your production host is on an older kernel that doesn't support io_uring well.
- You need PHP extensions not included in the image and prefer to extend your own base.
- You're on a PHP-FPM setup (not using Octane) — this image is specifically for Octane's long-running worker model.
The image on Docker Hub
The image is public and free to use:
docker pull roshankc1234/optimized-laravel-runtime:php8.5-swoole6.2.2-iouring
Available tags:
php8.5-swoole6.2.2-iouring— multi-arch manifest (amd64 + arm64). This is the tag to use in production — Docker selects the right layer automatically for your host architecture.php8.5-swoole6.2.2-iouring-arm64— arm64 only (AWS Graviton, Apple M-series in CI, Ampere Altra). Use this if you want to explicitly pin to arm64 and skip the manifest lookup.
Which tag should you use? On Graviton or any arm64 host, either tag works — the manifest tag is simpler. If you're cross-building on x86 CI targeting arm64 production, use --platform linux/arm64 with the manifest tag, or pull the dedicated arm64 tag directly.
Full details, Dockerfile, and configuration examples: hub.docker.com/r/roshankc1234/optimized-laravel-runtime.
Key takeaways
- io_uring reduces syscall overhead at high concurrency — benchmarks show 10–50% throughput improvement for I/O-heavy workloads vs epoll, with larger gains as connection count grows.
- The official
phpswoole/swooleimage doesn't compile io_uring for broad compatibility reasons. This image rebuilds Swoole 6.2.2 with--enable-iouringand strips compilers from the runtime layer. - ARM64 is the preferred architecture. The image ships a multi-arch manifest (amd64 + arm64). On AWS Graviton3, io_uring + arm64 delivers the best throughput per dollar — Graviton3 is ~20–40% more cost-efficient than equivalent x86 EC2 while running io_uring at full capability on its 6.1 kernel.
- Image includes PHP 8.5, Swoole 6.2.2, Nginx, Supervisor, and common extensions in a ~78MB Alpine base. Compressed size is the same for both architectures.
- Requirements: Linux kernel ≥ 5.12, Docker with io_uring syscalls allowed. Amazon Linux 2023 (arm64 or x86), Ubuntu 22.04+, and Debian 12 all qualify.
- Verify with
php --ri swoole | grep uring. If you hit a seccomp block, addio_uring_setup,io_uring_enter, andio_uring_registerto your Docker seccomp allowlist.
Questions, issues, or feedback on the image? Reach out — I actively maintain it and want to know what extensions or configuration options would make it more useful.