CloudPloy

Microservices Hosting on CloudPloy

CloudPloy is designed for applications running in Docker containers, which makes it well-suited for microservices architectures. Each service runs as a separate CloudPloy application in its own container with its own environment variables, deployment pipeline, and scaling configuration. Services on the same server communicate over the internal network without external exposure. This page covers how to structure and deploy multi-service applications on CloudPloy.

How Microservices Work on CloudPloy

In CloudPloy's architecture, a "microservice" is simply a CloudPloy application: a Docker container connected to a Git repository that deploys automatically on push. To run multiple services, you create multiple CloudPloy applications, each pointing to the relevant directory or repository for that service.

CloudPloy is not a Kubernetes cluster manager. It does not schedule containers across multiple nodes or manage service meshes. What it provides is simpler and more appropriate for most PHP and Node.js microservice architectures: isolated containers on managed servers with automatic deployments, health checks, and zero-downtime updates.

Architecture Patterns

Multiple Applications on One Server

The most common pattern: all services run on the same CloudPloy server, each as a separate application. They share the server's resources and communicate over the Docker bridge network using the server's internal IP address.

Example for a typical SaaS application:

Service Technology Port Purpose
api Laravel Internal (Nginx proxied) REST API for the frontend
worker Laravel queue worker No HTTP port Processes queued jobs (emails, exports)
scheduler Laravel scheduler No HTTP port Runs cron-like tasks every minute
websocket Node.js Internal (Nginx proxied) Real-time events via WebSocket
admin Laravel + Filament Internal (Nginx proxied) Admin panel on a separate subdomain

Each service has its own domain (or subdomain), its own Nginx vhost configuration managed by CloudPloy, and deploys independently when its code changes.

Services on Separate Servers

For larger workloads where services need to scale independently, deploy each service to its own CloudPloy server. A compute-heavy background processing service doesn't need to share CPU with the web-facing API. Traffic between servers goes over AWS's private network if you use the same AWS region - inter-server latency is typically under 1ms within the same availability zone.

Service Communication

HTTP/REST on the Same Server

Services on the same server communicate via HTTP using the server's internal IP (the Docker bridge address, typically 172.17.0.1). Each CloudPloy application listens on a port configured in its Nginx vhost. Other services call that port directly over the internal network - no external DNS lookup needed:

# Service A calling Service B's internal API
curl http://172.17.0.1:8001/api/users

# Or use a dedicated domain routed internally
curl http://users-service.internal/api/users

Alternatively, configure a private internal domain in your server's /etc/hosts file to give services stable hostnames that don't depend on IP addresses.

Message Queues with Redis

For asynchronous inter-service communication, use Redis as a message broker. Each service connects to the same Redis instance on the server and publishes/consumes messages from queues. This decouples services and provides natural back-pressure when downstream services are slower than upstream producers:

// Service A: Publish an event
Redis::publish('order.created', json_encode([
    'order_id' => $order->id,
    'customer_id' => $order->customer_id,
]));

// Service B: Subscribe and process
$redis->subscribe(['order.created'], function ($message) {
    $data = json_decode($message, true);
    $this->processNewOrder($data['order_id']);
});

Shared Database

The simplest integration pattern: services share a MySQL database and communicate by reading each other's tables. This avoids network hops and is straightforward to implement but creates tight coupling. Use it for services that are tightly related and owned by the same team. Avoid it for services intended to evolve independently.

Worker Services

Queue workers are among the most common "microservices" for PHP applications. Rather than configuring workers inside your main application container, run them as separate CloudPloy applications. This separates concerns and lets you scale workers independently.

Laravel Queue Worker Service

Create a separate CloudPloy application pointing to the same repository with a different start command:

# Dockerfile for the worker service
FROM cloudploy/php:8.3-fpm

WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts
COPY . .
RUN composer run-script post-autoload-dump

# Override the default PHP-FPM command with the queue worker
CMD ["php", "artisan", "queue:work", "redis", "--sleep=3", "--tries=3", "--max-time=3600"]

The worker container doesn't need Nginx or PHP-FPM. It just runs the artisan command. Configure it as a CloudPloy application with health monitoring disabled (it's a background process, not a web server). Use --max-time=3600 to restart the worker hourly and reclaim any accumulated memory.

Scaling Workers

To process more jobs concurrently, increase the replica count in App > Settings > Workers. Each replica is a separate container running the same worker command. CloudPloy manages the lifecycle of each replica independently - crashed workers restart automatically.

API Gateway Pattern

For multi-service deployments where external clients need a single entry point, deploy an Nginx-based API gateway as another CloudPloy application. The gateway proxies requests to the appropriate service based on the URL path:

# Nginx gateway configuration
server {
    listen 80;
    server_name api.yourdomain.com;

    location /users/ {
        proxy_pass http://172.17.0.1:8001;  # users-service
    }

    location /orders/ {
        proxy_pass http://172.17.0.1:8002;  # orders-service
    }

    location /payments/ {
        proxy_pass http://172.17.0.1:8003;  # payments-service
    }
}

CloudPloy adds SSL termination on top of this, so external traffic hits HTTPS at the gateway. Internal service-to-service communication remains HTTP on the private network.

Deploying Services from a Monorepo

When all services live in one repository (monorepo), each CloudPloy application uses the same repository but a different subdirectory as the build context. Configure the build context path in App > Settings > Build to point to the relevant service directory:

monorepo/
  services/
    api/           <-- CloudPloy app 1: build context = services/api/
      Dockerfile
      composer.json
    worker/        <-- CloudPloy app 2: build context = services/worker/
      Dockerfile
    websocket/     <-- CloudPloy app 3: build context = services/websocket/
      package.json
      Dockerfile

Each service deploys independently when its code changes, even though they share the same repository.

Environment Configuration

Each CloudPloy application has its own environment variable store. Services that need to communicate with each other need the other service's hostname or IP configured as an environment variable:

# Environment variables for the API service
USERS_SERVICE_URL=http://172.17.0.1:8001
ORDERS_SERVICE_URL=http://172.17.0.1:8002
REDIS_HOST=172.17.0.1
REDIS_PORT=6379
DB_HOST=172.17.0.1
DB_PORT=3306

Use the server's Docker bridge IP (172.17.0.1 by default) when services on the same server need to call each other. This IP address is stable for the lifetime of the server - it doesn't change between deployments or container restarts.

Health Checks for Each Service

Configure a health check endpoint for each web-facing service. CloudPloy polls this endpoint every 30 seconds and restarts the container if it fails consistently. For web services, return a 200 status with the service's dependency status:

// Laravel health check controller
public function health(): JsonResponse
{
    $checks = [
        'database' => $this->checkDatabase(),
        'redis' => $this->checkRedis(),
        'queue' => $this->checkQueueDepth(),
    ];

    $healthy = !in_array(false, $checks);

    return response()->json(
        ['status' => $healthy ? 'healthy' : 'degraded', 'checks' => $checks],
        $healthy ? 200 : 503
    );
}

Logging Across Services

Each CloudPloy application has its own log stream in App > Logs. All services should log to stdout/stderr so CloudPloy captures and displays the logs. For distributed tracing across services, include a correlation ID in every request and log it with each operation:

// Add a correlation ID middleware in Laravel
class CorrelationIdMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $correlationId = $request->header('X-Correlation-ID', Str::uuid()->toString());

        // Make it available to downstream service calls
        config(['app.correlation_id' => $correlationId]);

        // Add to all outgoing HTTP calls
        Http::withHeaders(['X-Correlation-ID' => $correlationId]);

        $response = $next($request);
        $response->header('X-Correlation-ID', $correlationId);

        return $response;
    }
}

With correlation IDs in place, you can search across service logs by ID to trace a request's path through your system.

When to Use Microservices

Microservices add deployment and operational complexity. For most PHP applications, a well-structured monolith with separate worker and scheduler processes is simpler to operate and perform just as well. Consider breaking into separate services when:

  • Different parts of the application have significantly different scaling requirements (the API scales independently from the batch processor)
  • Teams own different services and need independent deployment cycles
  • A specific service needs a different technology stack (a Python ML service alongside a PHP API)
  • You need strict isolation for security or compliance reasons (payment processing in a separate service)

If the main reason is "microservices are modern architecture" - that's not a sufficient reason given the operational overhead. Start with a modular monolith and extract services when the need becomes concrete.