Skip to content 99% OFF 🎉 Anniversary Sale 99% OFF Shared Hosting Use Code HURRYUP Claim Offer 99% OFF Hosting
99% OFF Hosting — Code HURRYUP
Products
AI Website Builder New VPS Hosting Cloud Servers Web Hosting cPanel Hosting Dedicated Servers Domains
Company
About Documentation Support Center Contact Get Started Call +91 75795 45488
Login
Hosting Panel — cPanel & Billing Console Panel — VPS Management
ALL SYSTEMS OPERATIONAL
VPS

Docker Compose on a VPS: Fix the Common Pitfalls

Getwebup 7 min read

Docker Compose looks simple until you put it on a real VPS next to a real web stack. The docker-compose.yml that worked fine on your laptop suddenly loses data on reboot, fights with Nginx over port 80, or writes files your web server user can't read. None of that is Docker being flaky — it's a handful of predictable gaps between a dev laptop and a production box. Here's what actually goes wrong and how to fix each one.

Issue 1: Containers don't come back after a reboot

Symptom: You reboot the VPS for a kernel update, come back an hour later, and the app is down. docker ps shows nothing running. No crash, no error in the logs — the containers just never started.

Cause: Compose's default restart policy is no. Docker only restarts a container automatically if you told it to. A reboot stops the Docker daemon along with everything it was running, and without a restart policy nothing tells those containers to come back up when the daemon does.

Fix: Set an explicit restart policy on every service in your compose file:

services:
  app:
    image: myapp:latest
    restart: unless-stopped
  db:
    image: postgres:16
    restart: unless-stopped

Then make sure the Docker daemon itself is enabled to start on boot:

sudo systemctl enable docker
sudo systemctl is-enabled docker   # should print "enabled"
PolicyBehaviour
noNever restarts automatically (the default)
on-failureRestarts only if the container exits with a non-zero code
unless-stoppedRestarts on crash or reboot, unless you manually stopped it
alwaysRestarts no matter what, even if you stopped it and didn't restart it before a reboot

For almost every production service, unless-stopped is the right call. always can fight you when you deliberately stop a container for maintenance and reboot before starting it again.

Issue 2: "port is already allocated" — Docker vs. your existing web server

Symptom: docker compose up fails with something like:

Error response from daemon: driver failed programming external connectivity on endpoint app:
Bind for 0.0.0.0:80 failed: port is already in use

Cause: Most VPS boxes we see this on already have Apache, Nginx, or a cPanel-managed web server bound to port 80/443 from before Docker was installed. Compose doesn't know or care — it just tries to grab the port and loses.

Fix: Decide which side owns port 80/443. Two clean options:

  • Let the host web server be the front door. Bind your container to an internal-only port and reverse-proxy to it: ports: ["127.0.0.1:8080:80"], then point Nginx or Apache at 127.0.0.1:8080 as an upstream.
  • Let Docker own the port. Stop and disable the host web server (sudo systemctl disable --now apache2) and let a container — often an Nginx or Traefik container — bind 80/443 directly.

Trying to run both on the same port at the same time is the one combination that never works. Check what's actually holding a port before you assume it's Docker's fault:

sudo ss -tulpn | grep ':80 '

Issue 3: Data disappears every time you run docker compose down

Symptom: You redeploy, run docker compose down && docker compose up -d, and the database is empty again. Uploads, sessions, everything reset.

Cause: The data was only ever living inside the container's writable layer, not in a volume. down removes containers (and their layers) by design; docker compose down -v is even worse and deletes named volumes too. If your compose file has no volumes: section for the database, there was never anywhere for the data to survive a container recreate.

Fix: Give stateful services a named volume, and never run -v against production:

services:
  db:
    image: mysql:8
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:

Confirm the volume actually persists across a recreate:

docker compose down
docker compose up -d
docker exec -it db_container mysql -u root -p -e "SHOW DATABASES;"

If you need the data on the host filesystem for your own backup tooling, use a bind mount instead of a named volume — but that brings you straight to the next issue.

Issue 4: "Permission denied" on bind-mounted volumes

Symptom: The app container can't write to a mounted folder, throws EACCES or Permission denied, even though the directory looks fine when you ls -la it from the host.

Cause: Most base images run as a non-root user with a UID that has nothing to do with the host account that owns the mounted folder. If your host directory is owned by ubuntu (UID 1000) but the container runs as UID 82 (common for PHP/www-data images) or a randomly assigned UID, the container's user simply isn't allowed to write there — permissions are enforced by UID number, not by username, and those don't line up across the host/container boundary.

Fix: Match ownership to whatever UID the container actually runs as. Find it first:

docker compose run --rm app id
# uid=82(www-data) gid=82(www-data) groups=82(www-data)

Then chown the host directory to match:

sudo chown -R 82:82 ./uploads

Or, if the image supports it, pin the container to run as your host user instead:

services:
  app:
    user: "1000:1000"

Do this once when you first wire up the volume, not after you've already lost a few hours to a mystery 500 error.

Issue 5: One service can't reach another by hostname

Symptom: Your app container throws could not translate host name "db" to address or connects to localhost and gets refused, even though both containers are clearly running.

Cause: Compose puts every service in a project on its own bridge network and registers each service name as a DNS entry on that network. localhost inside the app container refers to the app container itself, not the db container next to it — that's a different machine as far as networking is concerned. The service name is the hostname to use, and it only resolves for containers on the same compose network.

Fix: Point connection strings at the service name, not localhost or an IP:

DB_HOST=db
DB_PORT=3306

If two compose files need to talk to each other (a common setup when the app and a monitoring stack are deployed separately), put them on a shared external network:

networks:
  shared_net:
    external: true

and reference shared_net in both files. Without that, each docker compose up gets its own isolated network by default and the two stacks simply can't see each other.

Issue 6: .env values aren't showing up in the container

Symptom: You update a value in .env, redeploy, and the app is still using the old setting — or an entirely blank one.

Cause: Two separate mechanisms get confused for one. A top-level .env file next to your compose file is used for variable substitution inside the YAML itself (things like ${IMAGE_TAG}). It does not automatically become environment variables inside the container unless you also list them under environment: or env_file: for that service. And even when it's wired up correctly, Compose only re-reads changed values on a recreate, not on a plain restart.

Fix: Be explicit about which file feeds the container:

services:
  app:
    env_file:
      - .env

After changing values, force a recreate rather than a restart:

docker compose up -d --force-recreate app

A plain docker compose restart reuses the existing container as-is, environment and all.

Prevention checklist

  • Set restart: unless-stopped on every long-running service before you forget.
  • Decide up front whether Docker or the host web server owns ports 80/443 — don't let it get decided by whichever one starts first.
  • Put a named volume on anything that stores data you'd be upset to lose.
  • Check the container's actual UID before wiring up a bind mount, not after a permissions error.
  • Use service names for inter-container hostnames, never localhost.
  • Keep .env and env_file straight, and recreate (not just restart) after changing values.
  • Run docker compose config before deploying — it prints the fully resolved config and catches most of these mistakes before they hit production.

If you're moving an existing Compose stack onto a Getwebup VPS, our support team can review the compose file with you before go-live and flag exactly this kind of gap — it's a five-minute check that saves a much longer incident later.

Frequently asked questions

Should I use `docker-compose` (with a hyphen) or `docker compose`?

Use `docker compose` (the space-separated form). It's the current V2 plugin built into the Docker CLI and what's installed on new Ubuntu/AlmaLinux VPS images. The hyphenated `docker-compose` is the older standalone Python tool — it still works if installed separately, but it's no longer getting new features and isn't preinstalled on most fresh servers.

Do I still need a firewall if my services are only reachable through Docker's internal network?

Yes. Any port you publish with `ports:` in your compose file is exposed to the whole internet by default, and Docker manages this through iptables rules that sit ahead of UFW — a plain `ufw deny` on that port won't actually block it. Bind sensitive ports to `127.0.0.1` only, or route them through a reverse proxy, rather than relying on the host firewall alone.

How do I see logs for a specific service without wading through all of them?

Run `docker compose logs -f servicename` — the `-f` follows new output, and naming the service filters out the noise from everything else in the stack.

My compose file works locally but fails on the VPS with 'no such image'. Why?

Locally you likely built the image with `docker compose build`, but the image only exists in your laptop's local Docker registry. On the VPS, either push the image to a registry (Docker Hub, GHCR, or a private one) and reference it by tag, or copy the project over and run `docker compose build` on the server itself before `up`.

Is it safe to run `docker compose down -v` to 'clean up' on a live server?

No — the `-v` flag deletes named volumes along with the containers, which means your database and any persisted uploads go with it. Use plain `docker compose down` for a normal stop-and-remove, and only reach for `-v` on a throwaway dev environment where losing the data is fine.

#docker-compose #vps #docker #containers #linux #server-management

Keep reading

Chat with Support