Docker on a single server is not orchestration and is not a cloud. It is a way to install software without installing it: one command brings down a tested image, runs it as an isolated process tree with its own filesystem, and leaves your operating system with nothing on it but Docker. Nothing is deployed for you, nothing scales, nothing heals. What you get is repeatability - the same container behaves the same way on your laptop and on the box in Germany - plus the ability to run two versions of the same database without a fight.
The whole job on one machine comes down to five decisions per container: which image and which tag, which ports are published and to what address, where the data lives, what happens when it exits, and what stops it eating the disk. This post covers those five in order, with the commands, and then the ways people lose data.
What Docker changes on a single server#
The honest summary of what you gain:
- Installation becomes a pull. No PPAs, no compiling, no "this needs Python 3.9 and the box has 3.11". The image carries its own userland.
- Versions stop fighting. PostgreSQL 15 and 17 can run side by side on one machine, each with its own data directory and its own port, and neither one knows.
- Removal is complete.
docker rmtakes the process and the filesystem with it. Nothing is left in/usr,/etcor a package database. - Configuration becomes a command you can write down. The
docker runline is the install instructions, and you can keep it in a file - which is what Docker Compose for small stacks is.
And what it does not give you, which matters more because people assume otherwise. Containers are not virtual machines: they share the host kernel, so a kernel panic or an out-of-memory event on the host takes everything with it. They are not a security boundary against root - anyone who can run docker can mount the host filesystem into a container and read anything. They do not make software faster; the overhead is close to zero but so is the benefit. And they do not back anything up.
One prerequisite: this needs a real kernel of your own. On KVM, VMware, Hyper-V or Xen, Docker works. On container virtualisation such as OpenVZ or LXC it ranges from awkward to impossible. Run systemd-detect-virt before you plan around it - VPS, VDS or dedicated server goes through what that one word of output tells you about the plan you bought.
Installing Docker, and the group that is really root#
Two supported paths. The convenience script is fine on a machine you own:
$ curl -fsSL https://get.docker.com -o get-docker.sh$ sudo sh get-docker.sh$ sudo systemctl enable --now docker$ docker versionThe repository method is the same packages with more steps, and it is what you want in anything you will repeat. Either way the pieces you end up with are docker-ce (the daemon), docker-ce-cli (the command), containerd.io (the runtime underneath), and the docker-buildx-plugin and docker-compose-plugin subcommands. Avoid your distribution's own docker.io package: it lags behind and does not reliably bring the Compose plugin with it, which you will want within a week.
Then the part everyone skips:
$ sudo usermod -aG docker deploy$ newgrp docker$ docker run --rm hello-worldIf docker run hello-world prints the welcome text, the daemon is up, the network works and image pulls work. That is three tests in one command, and it is worth doing before you debug anything more complicated.
Images and containers: the first run#
An image is a stack of read-only filesystem layers plus metadata saying what to execute. A container is one running instance of an image with a thin writable layer on top. Deleting the container deletes that writable layer - which is why anything you care about has to live in a volume, three sections down.
$ docker run -d \ --name web \ --restart unless-stopped \ -p 127.0.0.1:8080:80 \ -v /srv/web/html:/usr/share/nginx/html:ro \ nginx:1.27-alpineReading that line left to right: run detached, call it web, bring it back after a reboot, publish container port 80 as port 8080 on loopback only, mount a host directory read-only, from the nginx image at tag 1.27-alpine.
The tag is a decision, not decoration:
| Tag style | Example | Use it when |
|---|---|---|
latest | nginx | Never on a server |
| Major | nginx:1 | You trust the project's compatibility promise |
| Minor | nginx:1.27 | Sensible default: security fixes, no surprises |
| Exact | nginx:1.27.2 | You want to choose every upgrade yourself |
| Digest | nginx@sha256:... | The build must be byte-identical every time |
latest is not a channel and carries no promise. It is simply the tag applied when nobody specified one, and it moves whenever the maintainer feels like it. A docker pull six months later can hand you a new major version, and the first you hear of it is a container that will not start. Pin at least the minor version on anything that holds data.
Day-to-day commands, all of which you will use hourly:
$ docker ps # running containers$ docker ps -a # including the dead ones$ docker logs -f --tail 100 web # follow the output$ docker exec -it web sh # a shell inside it$ docker stop web && docker rm web$ docker inspect web # every setting, as JSONdocker stop sends SIGTERM, waits ten seconds, then sends SIGKILL. If your application needs longer to shut down cleanly - flushing a write, finishing a request, saving a world - raise it with docker stop -t 60 and make sure the process actually handles SIGTERM. Graceful shutdown and health checks covers what handling it properly looks like in application code.
Ports, publishing and the firewall#
-p sets up destination NAT from a host port to a container port. Written as -p 8080:80, it binds to every address on the machine, including the public one. Written as -p 127.0.0.1:8080:80, it binds to loopback only.
Prefer the second form. Almost everything you run belongs behind something else - a reverse proxy for anything speaking HTTP, nothing at all for a database that only your application uses. The rule is: publish a port only if the internet is supposed to reach it directly.
This is not a style preference. Published ports bypass UFW. A container published with -p 8080:80 is reachable from the internet even when ufw status reports a default-deny policy with no rule for 8080, because the packet is forwarded to the container rather than delivered to the host and Docker's forwarding rules are evaluated first. The full explanation and the four ways around it are in the UFW firewall guide; the one-line version is to publish to 127.0.0.1 and let a proxy on the host handle the public side.
Containers that need to talk to each other should share a user-defined network instead of publishing anything:
$ docker network create appnet$ docker run -d --name db --network appnet -e POSTGRES_PASSWORD=... postgres:17$ docker run -d --name app --network appnet -p 127.0.0.1:3000:3000 my/app:1.4On a user-defined network, Docker runs an embedded DNS server, so the application connects to the host name db on port 5432 and nothing is exposed anywhere. The default bridge network does not do this name resolution, which is why "it works in Compose but not with docker run" is nearly always a missing docker network create.
Volumes, and where your data actually lives#
A container's writable layer dies with the container. There are two ways to keep data, and they are for different jobs.
Named volumes are managed by Docker and stored under /var/lib/docker/volumes/. Use them for anything a program owns and you only ever touch through that program: database files, a game world, an application's state directory.
$ docker volume create pgdata$ docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:17$ docker volume ls$ docker volume inspect pgdataBind mounts map a host path into the container. Use them for things you want to edit with your own editor: configuration files, a static site's HTML, a directory of uploaded media you also serve from nginx.
$ docker run -d --name web -v /srv/web/html:/usr/share/nginx/html:ro nginx:1.27-alpineThree practical points. The :ro suffix makes a mount read-only and costs nothing - use it wherever the container has no business writing. Bind mounts carry the host's ownership into the container, so a process running as UID 1000 inside the container cannot write to a directory owned by root outside it; either chown the directory to the right numeric ID or run the container with --user "$(id -u):$(id -g)". And a named volume that gets its initial contents from the image does so only when it is empty, which is why changing an image's default configuration after the first start appears to do nothing.
Backing up a volume is a container that mounts it and writes a tarball somewhere else:
$ docker run --rm -v pgdata:/data:ro -v /srv/backups:/out alpine \ tar czf /out/pgdata-$(date +%F).tar.gz -C /data .For a database, prefer the database's own dump tool over a filesystem copy of a live data directory - pg_dump through docker exec gives you a file that restores reliably, which a tarball of files being written to does not. pg_dump and pg_restore has the flags.
Restart policies, reboots and health#
A container does not come back on its own. By default, when the process exits or the machine reboots, it stays down.
| Policy | On crash | On docker stop | After a reboot |
|---|---|---|---|
no (default) | stays down | stays down | stays down |
on-failure:5 | restarts, up to 5 times | stays down | restarts if it was failing |
always | restarts | stays down until the daemon restarts | restarts |
unless-stopped | restarts | stays down | stays down if you stopped it |
unless-stopped is the right answer for nearly everything on a small server. It survives reboots and crashes, and it respects the fact that you stopped a container deliberately - which always does not, cheerfully bringing back the container you stopped an hour ago the next time the daemon starts.
You can change the policy on a running container without recreating it:
$ docker update --restart unless-stopped webRestart policies depend on the Docker service starting at boot, so confirm systemctl is-enabled docker says enabled. And note what a restart policy is not: it is not a health check. A container whose process is alive but wedged - a Node app that has stopped accepting connections, a game server stuck on a save - satisfies the restart policy perfectly. Docker's own HEALTHCHECK marks such a container unhealthy but does not restart it. If you want "restart when it stops answering", that is a job for a watchdog, or for a systemd unit wrapping the container with the health logic you want; systemd services for your apps has the unit file for that shape.
Limits and logs: the two things that quietly fill a box#
By default a container can use all the memory on the machine and all the CPU. On a server running one thing, that is fine. On a server running six, one runaway process takes the others with it.
$ docker run -d --name app \ --memory=1g --memory-swap=1g --cpus=1.5 \ my/app:1.4$ docker stats --no-streamSetting --memory-swap equal to --memory disables swap for that container, which is usually what you want: a process that has been pushed into swap is slow in a way that is harder to diagnose than an outright failure. When a container passes its memory limit the kernel kills the process inside it, the container exits with code 137, and docker inspect records it:
$ docker inspect -f '{{.State.OOMKilled}}' apptrueExit code 137 is 128 + 9, meaning SIGKILL. Exit code 143 is 128 + 15, a clean SIGTERM - usually you stopping it. Those two numbers answer most "why did it die" questions on their own. Linux swap and the OOM killer explains what the kernel is deciding when it picks a victim.
The quieter problem is logs. Docker's default logging driver writes everything a container prints to a JSON file under /var/lib/docker/containers/, with no size limit at all. A chatty application will fill the disk over weeks or months, and the symptom is a server that fails in several unrelated ways at once. Fix it globally:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }}Then sudo systemctl restart docker. Note that this applies to containers created afterwards; existing ones keep the settings they were created with, so recreate them or set --log-opt max-size=10m on each. Check what you are carrying today with sudo du -sh /var/lib/docker/containers/*.
Updating, pruning and disk space#
Updating a container is not an upgrade in place. You pull a new image, destroy the container and create a new one from the same volumes and the same run command:
$ docker pull nginx:1.27$ docker stop web && docker rm web$ docker run -d --name web --restart unless-stopped \ -p 127.0.0.1:8080:80 -v /srv/web/html:/usr/share/nginx/html:ro nginx:1.27This is exactly why writing the run line down matters, and exactly why Compose exists - docker compose pull && docker compose up -d does the same three steps from a file you already have. Keep the previous image tag until the new container has been running for a day; rolling back is then a matter of running the old tag again.
Disk usage creeps up in four places at once. This tells you where:
$ docker system dfTYPE TOTAL ACTIVE SIZE RECLAIMABLEImages 14 4 6.2GB 4.9GB (79%)Containers 7 4 112MB 43MB (38%)Local Volumes 6 3 2.1GB 820MB (38%)Build Cache 41 0 3.3GB 3.3GBdocker image prune -a removes images no container uses. docker builder prune clears the build cache, which on a box that builds its own images is often the largest single item. docker container prune removes stopped containers. Run those three on a schedule and you will rarely think about it again. Run docker system prune --volumes casually and you will eventually delete a database. The distinction is worth keeping in your fingers.
When a container will not start#
It exits immediately and `docker ps` shows nothing. Look at docker ps -a for the exit code and docker logs <name> for the reason. A container with no long-running process in the foreground exits as soon as its command finishes - that is not a fault, it is the design.
Port is already allocated. Something else has the host port. sudo ss -lntp | grep 8080 names it. Either stop it or publish on a different host port; the container port never has to change.
Permission denied on a mounted directory. UID mismatch on a bind mount. Check ls -ln on the host directory and the USER the image runs as, then align them.
The image will not pull. Check the tag actually exists, then check whether you are being rate limited - registries limit anonymous pulls by address, and docker login lifts that. On a fresh box also check DNS: containers use the host's resolver by default, and a broken /etc/resolv.conf shows up here first.
It worked yesterday and today it is a different version. You used latest. See the tag table.
Everything is slow and the disk is full. Logs or build cache. See the previous section.
RE:NODE's managed plans are containers too - the panel is a customised Pterodactyl with one container per server, a hard CPU throttle at the share you bought, and the same OOM behaviour described above, except that the whole container is stopped at the limit and restarted clean rather than left to swap. What you cannot do there is bring your own image, so if the software you want to run is not one of the catalogue lines, a VDS with root access and Docker on it is the honest answer. Choosing between a VDS and a game panel sets the two side by side, and what a hosting panel actually is covers the managed side.
FAQ#
Do I need Docker on a small VDS?
No, and for one application it can be an extra layer to learn for little gain. It earns its place the moment you run a second thing, need a version your distribution does not package, or want to move the same setup to a different machine unchanged.
How much overhead does a container add?
For CPU and memory, close to nothing - it is the same kernel running the same process with different namespaces. Container networking adds a small amount of latency through NAT, and a writable overlay filesystem is slower than a bind mount for heavy random writes, which is one more reason databases belong on volumes.
Where is my data if I delete a container?
In its volumes, which survive. Anything written inside the container but outside a volume is gone with the writable layer. Check what a container mounts with docker inspect before you remove it.
Can I run Docker inside a managed hosting plan?
No. A managed game or app plan is already a container, and you do not have a kernel or a daemon of your own inside it. Running Docker is one of the specific reasons to take a VDS instead.
Why is my container reachable from the internet when the firewall is on?
Because you published a port, and published ports are forwarded past the host firewall's input rules. Bind the publish to 127.0.0.1 unless the port is genuinely meant to be public, and put a reverse proxy in front of anything that is.
Should containers auto-update?
Not on a server you care about. Automatic image updates mean an unattended major version change at an hour nobody is watching. Pin a minor tag, pull deliberately, and keep the previous tag long enough to roll back.




კომენტარები
სრულიად ანონიმურად: ანგარიშის, ელფოსტის და cookie-ის გარეშე. ინახება მხოლოდ სახელი, ტექსტი და დრო - სხვა არაფერი. ბმულების რაოდენობა ლიმიტირებულია.