CloudPloy

Docker Deployment Tutorial for CloudPloy

Every application deployed on CloudPloy runs inside an isolated Docker container. This guide explains how containerization works on CloudPloy's infrastructure, how to write optimized Dockerfiles for PHP and WordPress applications, and how to troubleshoot container issues when things go wrong.

How CloudPloy Uses Docker

When you deploy an application through CloudPloy, the platform automatically:

  1. Pulls your code from your connected Git repository (GitHub, GitLab, or Bitbucket)
  2. Builds a Docker image using the Dockerfile in your repository root, or uses CloudPloy's optimized base image for your framework
  3. Runs the container on your provisioned server with the correct resource limits
  4. Wires up a reverse proxy (Nginx) in front of the container to handle SSL termination and traffic routing
  5. Configures health checks to restart your container if it becomes unresponsive

This architecture means your application behaves identically whether you are developing locally, testing on staging, or running in production - eliminating the classic "works on my machine" problem.

Prerequisites

  • A CloudPloy account with at least one provisioned server
  • Docker installed locally for building and testing images
  • A Git repository connected to CloudPloy
  • Basic familiarity with the command line

Part 1: Dockerfiles for PHP Applications

WordPress Dockerfile

CloudPloy provides a managed WordPress deployment that does not require you to write your own Dockerfile - the platform handles this automatically using its optimized WordPress base image. However, if you need custom PHP extensions, you can extend the base image:

# Extend CloudPloy's WordPress base image
FROM cloudploy/wordpress:php8.2

# Install additional PHP extensions
RUN docker-php-ext-install \
    bcmath \
    gd \
    intl \
    zip

# Copy custom php.ini settings
COPY docker/php.ini /usr/local/etc/php/conf.d/custom.ini

# Copy application code
COPY . /var/www/html/

Save this file as Dockerfile in your repository root. CloudPloy detects it automatically on the next deployment.

Laravel Dockerfile

For Laravel applications, a production-ready Dockerfile should use multi-stage builds to keep the final image lean:

# Stage 1: Install Composer dependencies
FROM composer:2.7 AS composer-build
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
    --no-dev \
    --no-scripts \
    --prefer-dist \
    --optimize-autoloader

# Stage 2: Build frontend assets
FROM node:20-alpine AS node-build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: Production image
FROM php:8.2-fpm-alpine

# Install system dependencies
RUN apk add --no-cache \
    nginx \
    supervisor \
    libpng-dev \
    libjpeg-turbo-dev \
    freetype-dev \
    zip \
    unzip

# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
 && docker-php-ext-install \
    pdo_mysql \
    pdo_pgsql \
    opcache \
    gd \
    bcmath \
    pcntl \
    redis

# Copy Composer dependencies from stage 1
COPY --from=composer-build /app/vendor ./vendor

# Copy compiled frontend assets from stage 2
COPY --from=node-build /app/public/build ./public/build

# Copy application code
COPY . .

# Set correct ownership
RUN chown -R www-data:www-data /var/www/html \
 && chmod -R 755 /var/www/html/storage

EXPOSE 9000

CMD ["php-fpm"]

General PHP Application Dockerfile

For plain PHP applications (Symfony, CodeIgniter, custom frameworks):

FROM php:8.2-apache

# Enable Apache modules
RUN a2enmod rewrite headers

# Install PHP extensions
RUN apt-get update && apt-get install -y \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip \
 && docker-php-ext-install \
    pdo_mysql \
    mbstring \
    exif \
    pcntl \
    bcmath \
    gd \
 && rm -rf /var/lib/apt/lists/*

# Configure Apache document root
ENV APACHE_DOCUMENT_ROOT /var/www/html/public
RUN sed -ri -e 's!/var/www/html!{APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
 && sed -ri -e 's!/var/www/!{APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf

# Copy application
COPY . /var/www/html/

# Set permissions
RUN chown -R www-data:www-data /var/www/html

EXPOSE 80

Part 2: Environment Variables and Secrets

Never bake credentials into your Docker image. CloudPloy provides a secure environment variable store accessible from the dashboard under App Settings > Environment Variables.

Set your application secrets there - database passwords, API keys, mail credentials - and they are injected into the container at runtime as environment variables. Your application reads them the standard way:

# Laravel .env example (these values come from CloudPloy at runtime)
DB_CONNECTION=mysql
DB_HOST={{MYSQL_HOST}}
DB_PORT=3306
DB_DATABASE={{MYSQL_DATABASE}}
DB_USERNAME={{MYSQL_USER}}
DB_PASSWORD={{MYSQL_PASSWORD}}

REDIS_HOST={{REDIS_HOST}}
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis

APP_KEY={{APP_KEY}}
APP_ENV=production
APP_DEBUG=false

CloudPloy replaces the placeholder variables above with the actual values from your environment variable store before starting the container.

Part 3: Persistent Storage with Volumes

Docker containers are ephemeral by default - any files written inside a container are lost when the container restarts. For WordPress and other applications that store user uploads, CloudPloy automatically mounts a persistent volume at:

  • WordPress: /var/www/html/wp-content/uploads
  • Laravel: /var/www/html/storage
  • Custom PHP: Any path you configure in App Settings > Volumes

These volumes persist across container restarts, image rebuilds, and server maintenance windows. They are also included in CloudPloy's automated backup system.

Adding Custom Volume Mounts

To mount additional directories, add them in the CloudPloy dashboard or via the API. Common use cases include:

  • Log directories you want to persist for audit trails
  • Generated PDF or export files that need to survive restarts
  • Shared file storage accessed by multiple app instances

Part 4: Health Checks

CloudPloy configures an HTTP health check on your container by default. The platform polls /health (or / if no health endpoint exists) every 30 seconds. If three consecutive checks fail, the container is restarted automatically.

For Laravel, add a dedicated health endpoint to your routes/web.php:

Route::get('/health', function () {
    // Check database connectivity
    try {
        DB::connection()->getPdo();
        $dbStatus = 'ok';
    } catch (\Exception $e) {
        $dbStatus = 'error';
    }

    // Check Redis connectivity
    try {
        Cache::store('redis')->set('health_check', true, 10);
        $redisStatus = 'ok';
    } catch (\Exception $e) {
        $redisStatus = 'error';
    }

    $status = ($dbStatus === 'ok' && $redisStatus === 'ok') ? 200 : 503;

    return response()->json([
        'status'    => $status === 200 ? 'healthy' : 'degraded',
        'database'  => $dbStatus,
        'cache'     => $redisStatus,
        'timestamp' => now()->toISOString(),
    ], $status);
});

You can also add a HEALTHCHECK instruction directly in your Dockerfile, which Docker itself will use:

HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD curl -f http://localhost/health || exit 1

Part 5: Zero-Downtime Deployments

CloudPloy performs zero-downtime deployments using a rolling strategy:

  1. A new container is built from your latest commit
  2. The new container starts and passes its health check
  3. Traffic is switched from the old container to the new one
  4. The old container is stopped and removed

For Laravel, this means database migrations must be backwards-compatible with the version currently running - because during the cutover, both old and new code may run simultaneously for a few seconds. Follow these rules:

  • Never drop a column in the same migration that removes code using it - do it in a separate deploy after the column is no longer read
  • Add nullable columns rather than non-nullable ones without defaults
  • Rename columns in two deploys: add the new column first, migrate data, then drop the old one in a follow-up deploy

Running Migrations Safely

Configure CloudPloy to run migrations before switching traffic. In your CloudPloy app settings, add this as a pre-deploy command:

php artisan migrate --force --no-interaction

CloudPloy runs this command inside the new container before routing traffic to it. If the migration fails, the deployment is aborted and your live container continues serving traffic.

Part 6: Multi-Container Applications

Applications that need a background queue worker, scheduler, or WebSocket server require multiple containers running the same image with different commands. Configure these in CloudPloy under App Settings > Workers:

Worker Name Command Instances
queue-worker php artisan queue:work --sleep=3 --tries=3 2
scheduler php artisan schedule:work 1
horizon php artisan horizon 1

Workers share the same environment variables and volume mounts as your main app container, so they have access to the same files and credentials.

Part 7: Image Optimization Best Practices

Use Alpine-Based Images

Alpine Linux images are significantly smaller than Debian/Ubuntu-based ones. php:8.2-fpm-alpine is around 30MB vs 400MB+ for the full Debian variant. Smaller images mean faster builds, faster deployments, and a reduced attack surface.

Use Multi-Stage Builds

Build tools like Composer, npm, and build-time dependencies should not end up in your production image. Multi-stage builds (shown in the Laravel example above) allow you to use a full build environment without including those tools in the final image.

Cache Dependencies Separately

Docker rebuilds layers from the point of the first change downward. Copy dependency files before your application code so they are cached independently:

# Good - dependency layer cached separately
COPY composer.json composer.lock ./
RUN composer install --no-dev
COPY . .

# Bad - any code change invalidates the composer install layer
COPY . .
RUN composer install --no-dev

Run as Non-Root User

Running as root inside a container is a security risk. Create a dedicated user for your application:

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

Part 8: Troubleshooting

Container Fails to Start

Open the CloudPloy dashboard and navigate to App > Logs > Build Logs to see the Docker build output. For runtime errors, check App > Logs > Application Logs. Common causes include:

  • Missing environment variables referenced by your application at startup
  • Failed database connection (wrong host or credentials)
  • Incorrect file permissions on storage or cache directories
  • A PHP extension required by your app is not installed in the image

Container Restarts in a Loop

If your container keeps restarting, the health check is likely failing. Check whether your /health endpoint returns a 200 status code. If you do not have a health endpoint, add one as shown above. You can also temporarily disable the health check in CloudPloy's app settings to determine if that is the cause.

Slow Build Times

If deployments are taking a long time to build, the most likely cause is a missing layer cache. Ensure your Dockerfile copies dependency manifests (composer.json, package.json) before copying application code. CloudPloy caches Docker build layers between deployments to speed up builds.

Out of Disk Space

Old Docker images and stopped containers accumulate over time. CloudPloy automatically prunes unused images after each successful deployment. If you are managing your own server and running low on disk, you can run:

# Remove all unused images, containers, networks, and build cache
docker system prune -af

Frequently Asked Questions

Can I use Docker Compose on CloudPloy?

CloudPloy manages container orchestration internally, so docker-compose.yml is not used directly. Instead, configure multi-container setups (workers, schedulers) through the CloudPloy dashboard. The platform handles service discovery, networking, and restarts automatically.

Can I SSH into a running container?

Yes. From the CloudPloy dashboard, go to App > Terminal to open an interactive shell inside your running container. This is useful for running one-off commands like php artisan tinker or inspecting log files.

How do I roll back a bad deployment?

Navigate to App > Deployments and click Rollback on any previous deployment. CloudPloy keeps the last 10 deployments available for instant rollback - no rebuild required, the previous image is already cached.

Does CloudPloy support custom Docker registries?

Yes. You can connect a private Docker registry (Docker Hub, AWS ECR, GitHub Container Registry, or GitLab Registry) under Server Settings > Container Registry. CloudPloy will pull your pre-built image from the registry instead of building from source.

What base images does CloudPloy provide?

CloudPloy maintains optimized base images for PHP 8.1, 8.2, and 8.3 (with FPM and Apache variants), WordPress (with WP-CLI pre-installed), and Node.js 18/20/22. These images are security-patched and updated regularly. See the full list in the CloudPloy documentation.

How do I install a PHP extension not available in the base image?

Extend the CloudPloy base image in your own Dockerfile and use docker-php-ext-install or pecl install. Push your custom Dockerfile to your repository and CloudPloy will build from it automatically.


Have questions about your specific Docker setup? Contact CloudPloy support or visit the documentation for framework-specific deployment guides.