SYSTEMS OPERATIONAL
VPS

Running Docker on a VPS: Setup and Common Fixes

Getwebup 6 min read

Docker on a VPS is one of the fastest ways to go from "I have a blank server" to "my app is running" — no more matching Node versions or fighting PHP extensions by hand. But it also brings a new set of problems that don't exist on a plain LAMP stack: permission denied on the socket, ports that won't bind, and disk space that vanishes into old images. Here's how we set it up cleanly on a fresh VPS, and how to fix the issues that come up right after.

Before you start: what your VPS needs

Docker isn't picky, but a few things matter more on a VPS than on a laptop:

  • At least 1GB RAM, though 2GB+ is a lot more comfortable once you're running two or three containers plus a reverse proxy.
  • A 64-bit Linux distro — Ubuntu 22.04/24.04 or Debian 12 are the safest bets for current Docker packages.
  • A non-root sudo user. If you're still logging in as root, set up a sudo user first — running containers as root on top of a root shell doubles your blast radius if something goes wrong.
  • Swap space if your plan is under 2GB RAM. Docker builds and multi-container stacks spike memory usage in short bursts, and without swap the OOM killer will pick a container to kill at the worst possible moment.

Installing Docker the right way

Skip apt install docker.io — it's usually several versions behind. Use Docker's official repository instead:

sudo apt update
sudo apt install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Confirm it's running and check the version:

sudo systemctl status docker
docker --version
docker compose version

Note the space, not a hyphen — docker compose is the current plugin syntax. The old standalone docker-compose binary still works on many servers but isn't getting new features.

Fixing "permission denied" on the Docker socket

Right after install, running docker ps as your regular user throws:

Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock

This isn't a bug — the socket is owned by root and the docker group by design. Add your user to that group instead of running everything with sudo:

sudo usermod -aG docker $USER
newgrp docker

Log out and back in (or run newgrp docker in the current shell) for the group change to take effect. One thing worth knowing: membership in the docker group is effectively root-equivalent, since anyone in it can mount the host filesystem into a container. Only add users you'd trust with sudo anyway.

Running your first container

A quick sanity check:

docker run --rm hello-world

For something closer to real use — say an Nginx container serving on port 8080:

docker run -d --name web -p 8080:80 --restart unless-stopped nginx:alpine

--restart unless-stopped matters more on a VPS than anywhere else: it means the container comes back up automatically after a reboot or a Docker daemon restart, without you having to SSH in and start it by hand.

Common issues after setup

"Port is already allocated"

Something else on the VPS is already bound to that port — often Apache or Nginx from a previous install, or another container. Find it before picking a random new port:

sudo ss -tulpn | grep :80
docker ps --format "table {{.Names}}\t{{.Ports}}"

If it's a system service like Apache, either stop it (sudo systemctl stop apache2) if you're moving fully to Docker, or map the container to a different host port, e.g. -p 8081:80, and put a reverse proxy in front to route by domain.

Containers can't reach each other

Containers started separately with docker run don't share a network by default and can't resolve each other by name. Either put them on a user-defined network:

docker network create appnet
docker run -d --name db --network appnet mysql:8
docker run -d --name app --network appnet -p 3000:3000 myapp

or, better, define both in a single docker-compose.yml — Compose creates a shared network automatically and lets services address each other by their service name.

Disk filling up from images and old containers

This is the one that catches people weeks in, not on day one. Every build layer, stopped container, and unused image sits on disk until you clean it up. Check what Docker is actually using:

docker system df

Then clear what's genuinely unused:

docker container prune
docker image prune -a
docker volume prune
# or, all at once:
docker system prune -a --volumes

Be careful with --volumes on that last one — it removes unused named volumes too, and that includes database data if the container using it isn't currently running. Check docker volume ls first if you're not sure what's attached to what.

Container exits immediately after starting

Check the logs before anything else:

docker logs web
docker logs --tail 50 web

Nine times out of ten it's a missing environment variable, a config file the container expects but wasn't mounted, or the main process finishing and exiting (Docker containers stay alive only as long as their main process runs — a container isn't a VM). If the app needs to stay resident, its entrypoint needs to be a long-running process, not a one-off script.

Putting Docker behind a reverse proxy

Running multiple sites or apps on one VPS means containers shouldn't bind directly to port 80/443. Install Nginx (or Traefik, if you want automatic SSL and service discovery) on the host, and proxy to each container's internal port by domain name. A typical Nginx server block for a container on port 3000:

server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Then issue a Let's Encrypt certificate with Certbot against that server block as usual. This keeps SSL termination and domain routing on the host, while each container just worries about its own app.

Keeping it healthy long-term

  • Update the Docker engine periodically with sudo apt update && sudo apt upgrade docker-ce — security patches land there too, not just in your images.
  • Pin image versions (nginx:1.25-alpine, not nginx:latest) in anything you'd call production, so a base image update doesn't silently change behavior on your next redeploy.
  • Schedule docker system prune as a monthly cron job on VPS plans with limited disk, so image buildup doesn't sneak up on you.
  • Back up named volumes separately from your regular file backups — a full VPS snapshot captures them, but a plain rsync of your project folder usually doesn't.

Docker makes a VPS a lot more flexible once it's set up correctly, but it does shift some of the operational burden from "configuring PHP-FPM" to "managing images and volumes." Get the basics above right and most of the day-to-day surprises go away.

Frequently asked questions

Do I need a minimum VPS size to run Docker?

1GB RAM is the practical floor, and even then you should add swap. 2GB or more is more realistic once you're running a database container alongside your app and a reverse proxy.

Can I run Docker alongside cPanel or an existing LAMP stack on the same VPS?

Technically yes, but it gets messy fast — both will compete for ports 80/443 and system resources. It's cleaner to either dedicate the VPS to Docker or keep cPanel and Docker workloads on separate servers.

Why does my container lose data after I remove it?

Anything written inside a container's own filesystem disappears when the container is removed. Use a named volume or bind mount for anything that needs to persist, such as a database's data directory.

Is docker-compose still relevant, or should I use docker compose?

Use the built-in `docker compose` (with a space) — it's the actively maintained plugin form. The standalone `docker-compose` binary still runs on many servers but is in maintenance mode.

How do I stop a runaway container from filling up my VPS's disk with logs?

Set a log size limit in daemon.json or on the container itself, e.g. `docker run --log-opt max-size=10m --log-opt max-file=3 ...`, so JSON logs get rotated instead of growing indefinitely.

#docker #vps #containers #linux #server-setup #troubleshooting

Keep reading

Chat with Support