Client Area

Docker Basics for Shared-Hosting Developers (PHP + Node.js)

ByDomain India Team
9 min read22 Apr 20262 views

In this article

  • 1Why Docker, if you're deploying to cPanel?
  • 2Core concepts in 10 minutes
  • 3Install Docker
  • 4Dockerfile walkthrough — PHP example
  • 5Layer caching — the order matters

Docker Basics for Shared-Hosting Developers (PHP + Node.js)

Docker is usually marketed to developers targeting production Kubernetes clusters. But even if your production is on our shared cPanel or DirectAdmin, Docker radically improves your local development setup — matching production more closely, onboarding new developers in minutes instead of hours, and keeping your laptop clean of project-specific dependencies. This article explains Docker the pragmatic way, from a PHP / Node.js developer's perspective.

Why Docker, if you're deploying to cPanel?

The classic "works on my machine" problem: your dev laptop has PHP 8.1, the staging server has PHP 8.2, production has PHP 8.3. Bugs appear and disappear based on where the code runs. Docker pins the runtime so every machine runs the same environment.

Benefits for shared-hosting developers:

  • Your laptop stays clean — no 5 versions of PHP installed
  • New developers onboard with one command
  • You catch production-only bugs on your laptop (match PHP version, extensions, MySQL version)
  • Same config for local + staging + CI — fewer environment-specific surprises

You don't run Docker in shared-hosting production; you replicate what your Docker setup does via cPanel's Select PHP Version, MySQL, etc.

Core concepts in 10 minutes

Image — a blueprint. Layers of filesystem changes that, applied in order, produce a runnable environment. Immutable.

Container — a running instance of an image. Can be started, stopped, deleted. Multiple containers can run from the same image.

Dockerfile — the recipe that builds an image. Plain text file with step-by-step instructions.

Docker Hub — the default image registry. Holds base images (php:8.2-apache, node:20-alpine, mysql:8).

Volume — persistent storage attached to a container. Your database data goes here — survives container restart and deletion.

Network — virtual network where containers can reach each other by name.

docker-compose.yml — multi-container definition. One file describes your whole app stack (app + database + cache).

Install Docker

  • macOS / Windows: Docker Desktop (docker.com/products/docker-desktop)
  • Linux: curl -fsSL https://get.docker.com | sh then sudo usermod -aG docker $USER

Verify:

bash
docker --version
docker compose version
docker run hello-world

Dockerfile walkthrough — PHP example

Create Dockerfile at your project root:

dockerfile
# Start from the official PHP 8.2 Apache image
FROM php:8.2-apache

# Install system dependencies and PHP extensions
RUN apt-get update && apt-get install -y \
        libpng-dev libjpeg-dev libfreetype6-dev \
        libzip-dev zip unzip \
        git \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install pdo_mysql gd zip bcmath \
    && rm -rf /var/lib/apt/lists/*

# Enable Apache modules
RUN a2enmod rewrite headers

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Set document root (for Laravel, public/ is the doc 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

WORKDIR /var/www/html

# Copy dependency files first (for layer caching)
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --no-interaction

# Now copy the rest of the app
COPY . .

RUN composer dump-autoload --optimize \
    && chown -R www-data:www-data storage bootstrap/cache

EXPOSE 80

CMD ["apache2-foreground"]

Build:

bash
docker build -t my-laravel-app .

Run:

bash
docker run -p 8080:80 my-laravel-app
# Visit http://localhost:8080

Layer caching — the order matters

Docker caches each RUN / COPY step as a layer. Steps after a changed layer are re-run. Copy dependency files first (they change rarely), then source code (changes every commit). Otherwise every code change invalidates your composer install cache.

Dockerfile — Node.js with multi-stage build

Multi-stage builds keep production images small by discarding build-time tooling:

dockerfile
# Stage 1: builder
FROM node:20-alpine AS builder
WORKDIR /app

# Copy deps first
COPY package.json package-lock.json ./
RUN npm ci

# Build
COPY . .
RUN npm run build

# Stage 2: production
FROM node:20-alpine AS production
WORKDIR /app

# Only copy what's needed to run
COPY --from=builder /app/package.json /app/package-lock.json ./
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/node_modules ./node_modules   # or re-install prod-only

# Run as non-root for security
RUN addgroup -S app && adduser -S app -G app
USER app

EXPOSE 3000
CMD ["node", ".next/standalone/server.js"]

Alpine images are ~5x smaller than Debian-based ones. Trade-off: musl libc instead of glibc, which occasionally breaks native Node modules — test before deploying.

docker-compose for local dev

One file defines your whole stack:

yaml
# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "8080:80"
    volumes:
      - ./:/var/www/html                 # mount source for hot-reload
      - ./docker/apache.conf:/etc/apache2/sites-available/000-default.conf
    environment:
      - DB_HOST=db
      - DB_NAME=myapp
      - DB_USER=myapp
      - DB_PASSWORD=secret
      - REDIS_HOST=redis
    depends_on:
      - db
      - redis

  db:
    image: mysql:8
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: rootsecret
      MYSQL_DATABASE: myapp
      MYSQL_USER: myapp
      MYSQL_PASSWORD: secret
    volumes:
      - db-data:/var/lib/mysql

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  mailhog:
    image: mailhog/mailhog
    ports:
      - "1025:1025"    # SMTP
      - "8025:8025"    # web UI

volumes:
  db-data:

Start the whole stack:

bash
docker compose up -d

Access your app at http://localhost:8080, MailHog at http://localhost:8025. All containers share a network — the app can reach the DB at hostname db (the service name).

Other useful commands:

bash
docker compose logs -f app          # tail logs for one service
docker compose exec app bash        # interactive shell in the app container
docker compose exec db mysql -u root -p      # DB shell
docker compose down                 # stop and remove containers (volumes persist)
docker compose down -v              # also remove volumes (wipe DB data)

Development workflow

  1. docker compose up -d — start stack
  2. Edit code in your IDE — changes are reflected in the container via the bind-mount
  3. docker compose exec app bash — run composer install, npm install, artisan commands
  4. docker compose exec db mysql -uroot -p — poke the DB
  5. docker compose logs -f — watch for errors
  6. docker compose down — stop when done

No more "hmm, this worked locally" — everyone in the team sees the exact same PHP version, MySQL version, extensions.

When NOT to use Docker

  • Production shared cPanel / DirectAdmin. Your host runs PHP directly; Docker adds nothing.
  • Trivial projects. If your project is index.php with two lines, Docker is overkill.
  • Developers learning programming for the first time. Docker adds concept overhead.
  • High-performance native code. Docker on macOS / Windows has I/O overhead for bind-mounts — up to 2–3× slower filesystem than native. Linux hosts don't have this problem.

Moving a Docker-developed app to cPanel production

You don't deploy the Docker image to cPanel. You replicate the environment:

  1. Your Dockerfile installs PHP 8.2 with specific extensions → on cPanel, use "Select PHP Version" to pick 8.2 and enable those extensions.
  2. Your Dockerfile installs Composer and runs composer install → do the same manually or via GitHub Actions CI.
  3. Your Dockerfile sets the document root to public/ → cPanel → Domains → set document root.
  4. Your docker-compose uses MySQL 8 → cPanel's MySQL 8 serves this role.

If you find yourself really wanting to deploy the container image itself, you've outgrown shared hosting — move to our VPS plans and run Docker on the VPS.

Common pitfalls

  1. Volume permissions mismatch on Linux. Host UID 1000 vs container UID 33 (www-data) causes "Permission denied" on writes. Fix: either match UIDs via build args, or chown in the Dockerfile.
  2. Port conflicts. 3306 already used by host MySQL, 6379 by host Redis. Change host-side port in ports: "3307:3306".
  3. Windows + WSL2 filesystem slowness. Use a WSL2-native filesystem path (e.g., /home/user/project/), not a mounted Windows path (/mnt/c/Users/...).
  4. Forgetting `.dockerignore`. COPY . . copies node_modules/, .git/, and secrets. Add .dockerignore:

```

.git

node_modules

vendor

.env

*.log

```

  1. Using `latest` tag in production. node:latest changes over time — you don't know which version you'll get. Always pin: node:20-alpine, php:8.2-apache.
  2. Building massive images. Alpine bases, multi-stage builds, --no-install-recommends in apt-get — keep images under 500 MB.
  3. Not running as non-root in production images. USER nobody or USER app at the end — if the container is ever compromised, the attacker doesn't have root inside it.
  4. Putting secrets in the Dockerfile. Dockerfile text is baked into the image. Use build-args only for non-secret things; use env vars / secret mounts for actual secrets.

Frequently asked questions

Can I run Docker on Domain India shared hosting?

No. Shared hosting doesn't let end users run arbitrary long-running processes or manage the kernel. For Docker in production, use our VPS hosting — full root access, any workload.

What's the difference between Docker and a VM?

VMs simulate full hardware + kernel. Docker containers share the host's kernel, isolating only processes and file systems. Docker is 10–100× more resource-efficient.

Should I use Docker Compose in production?

On a VPS — yes, for small-to-medium apps. For larger infrastructure, orchestrators like Kubernetes or Docker Swarm manage multi-host deployments.

How do I debug inside a container?

docker compose exec <service> bash drops you into an interactive shell. From there, tail logs, run PHP scripts, inspect the filesystem — same as SSH into a server.

Can I use the same image for dev and production?

Yes and no. Dev typically mounts source code for live-reload; production bakes source into the image. Use multi-stage Dockerfiles — one target for dev, another for production.

How do I handle database migrations with Docker?

Run the migration command after docker compose up: docker compose exec app php artisan migrate. In CI/CD, add it as a deploy step after starting the new container.

What about Podman / Colima as Docker alternatives?

Podman and Colima are Docker-compatible alternatives. Same commands, same Dockerfiles, different runtime. Podman is popular on Linux (daemonless); Colima is a lightweight Docker Desktop replacement on macOS.

Is Docker worth learning for a solo PHP developer?

Yes — it's a career-upgrading skill, and even solo it makes your local setup cleaner. Spend a weekend learning. Your future self will thank you.


Need help setting up a Dockerised local dev env that mirrors your DI hosting production? [email protected] — we provide best-effort guidance as part of standard VPS support.

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket