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 | shthensudo usermod -aG docker $USER
Verify:
docker --version
docker compose version
docker run hello-worldDockerfile walkthrough — PHP example
Create Dockerfile at your project root:
# 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:
docker build -t my-laravel-app .Run:
docker run -p 8080:80 my-laravel-app
# Visit http://localhost:8080Layer 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:
# 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:
# 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:
docker compose up -dAccess 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:
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
docker compose up -d— start stack- Edit code in your IDE — changes are reflected in the container via the bind-mount
docker compose exec app bash— runcomposer install,npm install, artisan commandsdocker compose exec db mysql -uroot -p— poke the DBdocker compose logs -f— watch for errorsdocker 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.phpwith 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:
- Your Dockerfile installs PHP 8.2 with specific extensions → on cPanel, use "Select PHP Version" to pick 8.2 and enable those extensions.
- Your Dockerfile installs Composer and runs
composer install→ do the same manually or via GitHub Actions CI. - Your Dockerfile sets the document root to
public/→ cPanel → Domains → set document root. - 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
- 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.
- Port conflicts. 3306 already used by host MySQL, 6379 by host Redis. Change host-side port in
ports: "3307:3306". - Windows + WSL2 filesystem slowness. Use a WSL2-native filesystem path (e.g.,
/home/user/project/), not a mounted Windows path (/mnt/c/Users/...). - Forgetting `.dockerignore`.
COPY . .copiesnode_modules/,.git/, and secrets. Add.dockerignore:
```
.git
node_modules
vendor
.env
*.log
```
- Using `latest` tag in production.
node:latestchanges over time — you don't know which version you'll get. Always pin:node:20-alpine,php:8.2-apache. - Building massive images. Alpine bases, multi-stage builds,
--no-install-recommendsin apt-get — keep images under 500 MB. - Not running as non-root in production images.
USER nobodyorUSER appat the end — if the container is ever compromised, the attacker doesn't have root inside it. - 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.