Your site is down. You ssh in, run docker compose up -d, and the container exits immediately. No error message you can parse, just a silent crash. Now what?
We see this at Canadian Web Hosting more often than you’d think. A customer sets up a self-hosted app on their Cloud VPS, follows a tutorial, everything works for a week, and then something breaks. The container won’t start, docker ps shows nothing, and the trail goes cold.
This guide walks through the most common Docker deployment failures in a systematic order — from simplest fix to deepest root cause. Each section includes the exact commands to diagnose the problem, what the output should look like, and how to fix it.
Quick Fix: The Logs Never Lie
Before anything else: check the container logs. In nine out of ten cases, the answer is in the last 50 lines.
docker logs <container-name> --tail 50
If the container exited immediately and docker ps -a shows it as Exited (1), use:
docker logs <container-name> 2>&1 | tail -30
What to look for:
Error response from daemon— Docker daemon couldn’t start the containerPermission denied— file or volume mount permissionsAddress already in use— port conflictno such file or directory— missing config file or volumedatabase connection refused— dependency container not ready
If you see any of these error patterns, jump to the relevant root cause section below. If the logs are empty or say nothing useful, your container likely never started — the issue is at the Docker engine or configuration level, not inside the app.
Ops Note: Debug in the Same Order Every Time
Docker failures become expensive when people jump straight to rebuilding images or reinstalling the app. In support work, the faster path is boring: check container state, read the last logs, confirm ports, inspect mounts, verify environment variables, then test dependencies. Keeping that order avoids destroying the evidence you need to find the actual cause.
Root Causes (Systematic Diagnosis)
Each of these causes includes three things: how to confirm it (exact diagnostic command + expected output), how to fix it, and how to verify the fix.
1. Port Conflict — “Port is Already Allocated”
How to confirm:
docker ps -a | grep <port>
# Or check which process is using the port:
sudo ss -tlnp | grep :8080
If ss returns a line showing docker-proxy or another PID using port 8080, you have a port conflict. Common causes: another container grabbed the port first, a system service (like Nginx or Apache) is listening on the same port, or the host firewall is redirecting traffic.
The fix:
Three options, in order of preference:
- Change the host port. Edit your docker-compose.yml to use a different host port:
ports: "8081:3000"instead of"8080:3000" - Stop the conflicting container:
docker stop <conflicting-container> - Stop the system service (if Nginx):
sudo systemctl stop nginx
Verify:
docker compose up -d
# Then check the port is listening:
curl -I http://localhost:8080
# Expected: HTTP/1.1 200 OK or similar
2. Volume Mount Permission Issues
How to confirm: Run docker logs <container-name> and look for:
Error: EACCES: permission denied, open '/app/data/config.json'
# or
failed to bind mount: permission denied
Why it happens: Docker runs as root inside the container, but the mounted volume on the host has different ownership (usually 1000:1000 for your SSH user). The app inside the container tries to write to a directory it doesn’t own.
The fix:
# Check the UID the container runs as (often in the Dockerfile or docs):
# Then set the host directory ownership to match:
sudo chown -R 1000:1000 ./data
# Or for most apps, just give broad write access:
sudo chown -R nobody:nogroup ./data
sudo chmod -R 775 ./data
A more durable fix is to set the container user explicitly in docker-compose.yml:
services:
myapp:
image: myapp:latest
user: "1000:1000" # Match your SSH user UID
volumes:
- ./data:/app/data
Verify:
docker compose down && docker compose up -d
docker logs <container-name> --tail 10
# Should show app starting normally, no EACCES errors
3. Missing or Incorrect Environment Variables
How to confirm:
docker logs <container-name> --tail 20
# Look for patterns like:
# "DATABASE_URL is not set"
# "Missing required environment variable"
# "ConnectionError: could not connect to server"
The fix: Compare your .env file or docker-compose.yml environment block against the app’s official documentation. Common missing variables:
DB_HOSTorDATABASE_URL— database connection stringREDIS_URL— caching/queue backendJWT_SECRETorAPP_KEY— authentication secretSMTP_HOST— email sendingNEXT_PUBLIC_prefixed variables (Next.js apps) — must be set at build time, not runtime
Verify: Add the missing variables to your .env file, run docker compose down && docker compose up -d, and check docker logs again. The error should change to a different failure or disappear entirely.
4. Database Dependency Not Ready (Race Condition)
How to confirm:
docker logs <app-container> --tail 20
# Output looks like:
# "cannot connect to database at db:5432"
# "connection refused"
# "dial tcp 172.X.X.X:5432: connect: connection refused"
Why it happens: Docker Compose starts all services simultaneously. The database container takes 10-30 seconds to initialize (especially the first time when it creates tables), but the app container tries to connect immediately and fails.
The fix — two approaches:
Approach A: depends_on with condition (Docker Compose v2.10+):
services:
app:
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U <username>"]
interval: 5s
timeout: 5s
retries: 5
Approach B: Wait script in the app container’s entrypoint: Many app containers include a wait script. Check docker exec for it, or add a simple bash loop:
# Inside the Docker entrypoint or a startup script:
until pg_isready -h db -U <user>; do
echo "Waiting for database..."
sleep 2
done
Verify:
docker compose down -v
docker compose up -d
# Watch logs until you see "database connected" or similar:
docker compose logs -f app
# Expected: app starts successfully, no connection errors
Avoid restart: always as a workaround for this — it creates a crash loop that fills logs and delays recovery.
5. Insufficient Memory or Resource Limits
How to confirm:
docker logs <container-name> --tail 20
# Look for:
# "Killed" — the container ran out of memory and was OOM-killed
# "Exit code 137" — SIGKILL from OOM killer
# "Cannot allocate memory"
# Check system resources:
free -h
df -h
docker stats --no-stream
Why it happens: Many self-hosted apps have higher memory requirements than their Docker images suggest. A Postgres container with default settings uses 256-512 MB just for buffer cache. Nextcloud indexes files in memory. Jellyfin transcodes in RAM. A 1 GB VPS can fill up fast.
The fix:
- Check if Docker has resource limits set:
docker inspect <container> | jq '.[0].HostConfig.Memory'— if this shows a number, the container is capped. - Increase or remove the cap in docker-compose.yml:
deploy: resources: limits: memory: 2G - Add swap if the system has no swap file:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# Make permanent:
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
Verify:
docker compose down && docker compose up -d
docker stats --no-stream --no-trunc | head -5
# Container memory should be within limits
free -h # Check available memory
6. Docker Image Architecture Mismatch
How to confirm:
# Check what architecture your VPS is:
uname -m
# Expected: x86_64 (Intel/AMD) or aarch64 (ARM/Apple Silicon)
# Check what the image supports:
docker manifest inspect <image> | grep architecture
The fix: Most Docker Hub images support multiple architectures via manifest lists. If the automatic selection fails, specify it explicitly:
# In docker-compose.yml:
services:
myapp:
image: myapp:latest@sha256:<specific-hash>
# Or using platform flag:
platform: linux/amd64
This is most common on ARM-based VPS systems or when pulling old images that don’t have multi-arch support.
Verify:
docker compose down && docker compose up -d
docker logs <container> --tail 5
# Expected: app starts normally, no exec format error
7. Corrupted Data or Config Volume
How to confirm: Check if the container starts fresh with a new volume but fails with the persistent one:
# Backup then clear the volume
mv ./data ./data.bak
mkdir ./data
docker compose up -d
# If the app starts now, the original volume had corrupted data
The fix — repair in stages:
- Check app-specific recovery docs — most self-hosted apps (Nextcloud, Vaultwarden, Jellyfin) have repair commands
- Restore from backup:
cp -a ./data.bak ./data-restoredthen selectively copy clean config - As a last resort, reinitialize with a clean database and import from your last known-good backup
Prevention: Use CloudSafe Backup or a nightly docker exec <db-container> pg_dump -U <user> <dbname> > backup.sql to protect database volumes.
Verify:
docker compose down && docker compose up -d
# App should start without errors
# Then restore any necessary data from backup
Diagnostic Flowchart
Not sure which root cause applies? Follow this decision tree:
| Step | Check | If Yes ? | If No ? |
|---|---|---|---|
| 1 | Container shows Exited (0)? |
Go to step 3. The app started and intentionally stopped (health check, init failure) | Go to step 2 |
| 2 | Container shows Exited (1) or Exited (137)? |
Read docker logs ? Root Cause 1-7 depending on error message | Try docker compose up (foreground) to see real-time output |
| 3 | Logs show “address already in use”? | Root Cause 1: Port Conflict | Go to step 4 |
| 4 | Logs show “permission denied” or “EACCES”? | Root Cause 2: Volume Permissions | Go to step 5 |
| 5 | Logs show “connection refused” to database? | Root Cause 4: Database Dependency | Go to step 6 |
| 6 | Logs show “Killed” or “cannot allocate memory”? | Root Cause 5: Memory Limits | Go to step 7 |
| 7 | Logs show “exec format error”? | Root Cause 6: Architecture Mismatch | Go to step 8 |
| 8 | Container starts with fresh volume but not persistent one? | Root Cause 7: Corrupted Volume | Try Root Cause 3: Missing Environment Variables |
Prevention
The best fix is the one you never need. These practices dramatically reduce the chance of deployment failures:
Set Up Basic Monitoring
Docker events are ephemeral — once a container exits, the log buffer is gone after docker compose down. Set up log rotation and external monitoring to capture failures as they happen:
# Docker daemon log rotation (in /etc/docker/daemon.json):
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
For a full monitoring setup, see our server monitoring guide and Portainer visual Docker management guide.
Test Changes in Isolation
Before updating a production container:
- Back up your volumes:
tar -czf app-backup-$(date +%F).tar.gz ./data - Test the new config on a separate port or staging compose file
- Keep the last known-good
docker-compose.ymlcommitted to git
Use Health Checks
Add Docker health checks to every service. This prevents the “running but not working” scenario where Docker thinks the container is healthy but the app inside is unresponsive.
services:
app:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
When to Escalate to Hosting Support
If you’ve gone through all seven root causes and the container still won’t start, it’s time to rule out infrastructure issues. Contact Canadian Web Hosting support if:
systemctl status dockershows the Docker daemon itself is failing- The VPS kernel logs show
Out of memory: Killed processeven with swap enabled - Disk I/O errors appear in
dmesg | grep error - The issue affects ALL containers, not just one app
CWH offers Managed Support for customers who prefer hands-off infrastructure — we handle Docker setup, monitoring, and troubleshooting so you can focus on your application.
Related Issues
If the app starts but has specific problems, check these related guides:
- Docker Logs Are Filling Your Server Disk: Diagnosis and Fix — when log files consume all available space
- Setting Up a Reverse Proxy with Nginx: A Complete Production Guide — for port conflict resolution and reverse proxy setup
- Portainer: Visual Docker Management for Your VPS — graphical container management
If your app started but something still feels wrong — slow performance, intermittent errors, or unexpected restarts — check our Linux disk full diagnosis guide and server monitoring explainer for ongoing health checks.
What will you run on your Cloud VPS? Canadian Web Hosting’s Cloud VPS includes full root access, SSD storage, Canadian data centres, and 24/7 support — everything you need to run Docker-based self-hosted applications. See our Cloud VPS plans ?
Email delivery not working? If your app starts but can’t send emails, check out our SMTP Troubleshooting Guide for Self-Hosted Apps for help with connection refused, authentication failures, and relay issues.
Sources and Command Notes
This troubleshooting guide was refreshed in June 2026. The Docker log, port, inspect, and curl examples were smoke-tested locally against a temporary nginx container. Docker Compose syntax and service health behavior should be checked against the current Compose specification before changing production files.
Be First to Comment