HostiServer
2026-09-16 11:18
Docker on a VPS: Installation and First Container
📚 "Docker on a VPS" series, part 1 of 5:
- Docker on a VPS: installation and your first container ← you are here
- Docker Compose: multiple services together
- Docker in production
- Docker + CI/CD: auto-deploy with GitHub Actions / GitLab CI
- Gitea / Forgejo in Docker: a full self-hosted stack
Docker on a VPS: installation and your first container
In the previous series, we automated the code delivery process itself: push to a branch, and within minutes the change is live on the server with no human involved. But the "push → SSH deploy → restart the service" scheme only cures one symptom. The second, no less painful one, stays exactly where it was: dependencies on the server still have to be installed by hand, and they conflict between projects living on the same VPS.
A classic situation: one project is written for Node.js 18 and depends on a specific major version of its packages, another is newer, already on Node.js 20. Both need to deploy to the same server. Without Docker, that's either two system-wide Node.js installs fighting each other through nvm and PATH variables, or a compromise where one of the projects runs on a version it was never tested against. The same thing repeats with PHP, Python, library versions, system packages — any shared dependency on the host eventually becomes a point of conflict.
Docker solves this at the architecture level, not through agreement. Every application gets its own isolated environment with its own dependencies, and that environment is described in code, not in the administrator's head. The same image you built and tested locally runs on the server bit-for-bit identical. And rolling back to a previous version isn't "remember which files to overwrite" — it's one line: docker run with the old image tag.
ℹ️ Related article: if you don't yet have a CI/CD pipeline set up for deployment, it's worth starting with "What CI/CD Is: From Manual Deploy to Automated Pipelines". Automating the deployment process and containerizing what gets deployed are two halves of the same job: making infrastructure managed by code rather than by a set of manual steps.
2. Key Docker concepts
Before installing anything on a server, it's worth getting familiar with five terms, without which no Docker command will make sense.
2.1 Image — an immutable template
An image is a read-only template a container is launched from: a filesystem, installed dependencies, environment variables, a default startup command. An image never changes on its own — node:20-alpine, nginx:latest, or your own my-app:1.0 stay the exact same bytes until you explicitly build a new image.
2.2 Container — a running instance
A container is a process launched from an image, with its own filesystem on top of the image's layers, its own network namespace, and its own PID namespace. You can launch as many containers as you like from a single image at once, and each will be isolated from the others, even if they come from the exact same image.
2.3 The layer system and build cache
Every instruction in a Dockerfile creates a separate layer, and layers get cached. If package.json hasn't changed since the last build, the npm install layer is pulled from cache without re-running — this is exactly the property that makes repeated CI builds many times faster than the first one.
2.4 Image registries
Docker Hub is the default public registry: docker pull nginx with no extra configuration downloads the official image from there. But it's not the only option.
| Registry | When it fits | Notable trait |
|---|---|---|
| Docker Hub | Public official images, small private projects | Rate limits on anonymous pulls for unauthenticated clients |
| GHCR (GitHub Container Registry) | Projects already living on GitHub | Access rights are managed through the same GitHub organization |
| Self-hosted registry | A closed perimeter, compliance requirements | Full control, but the administration is on you |
2.5 Docker Engine vs Docker Desktop
Docker Desktop is a graphical wrapper for local development on Mac and Windows, with its own virtual machine under the hood. On a VPS it isn't needed and doesn't even get installed: the server gets only Docker Engine — the daemon (dockerd) and the CLI, with no graphical layer whatsoever. It's the Engine we install in the next section.
3. Installing Docker on a VPS
3.1 Why not apt install docker.io
The simplest path turns out to be a trap. The docker.io package from Ubuntu's or Debian's standard repository is a version frozen at the distribution's release date — typically 20.x at a time when the current line is already 26.x and above. The difference isn't just a version number: older builds are missing security fixes and a chunk of modern BuildKit features.
3.2 The official way — the Docker Inc. repository
The fastest reliable option is the official install script:
curl -fsSL https://get.docker.com | sh
The script detects the distribution on its own, adds the official Docker repository, and installs the current Community Edition together with containerd and the CLI plugins (Compose, Buildx). For production servers where piping a script from the internet isn't a preferred option for security reasons, the same repository can be added manually following the official documentation for your specific distribution.
3.3 Running without sudo
By default, only root has access to the Docker daemon, and every command needs sudo. To work as a regular user:
sudo usermod -aG docker $USER
newgrp docker
Instead of newgrp docker you can simply log back in — the group change applies on the next login.
⚠️ On the security of the docker group: membership in the docker group is effectively equivalent to root access on the host, since the daemon runs with root privileges and anyone in the group can mount the host's root filesystem inside a container. Only add people to this group who genuinely need direct Docker access on this particular server.
3.4 Autostart after a reboot
The official install script already enables autostart, but it's worth confirming explicitly:
sudo systemctl enable docker
3.5 Verifying the installation
docker --version
docker info
docker run hello-world
The last command is the best quick test: if Docker can pull a tiny image and print a greeting, the daemon is running, permissions are set up correctly, and the network for pull requests is reachable.
4. First commands: hands-on with nginx
Theory only clicks after a few commands in the terminal. Let's walk through a container's lifecycle using nginx as an example.
4.1 Pulling an image
docker pull nginx:alpine
The tag is specified explicitly instead of latest — this matters for reproducibility: latest today and latest in a month can be different images, and a server hit by a stray untagged docker pull can one day get an unexpected major version.
4.2 Starting a container
docker run -d -p 80:80 --name my-nginx nginx:alpine
| Flag | Meaning |
|---|---|
-d |
Detached — runs in the background, the terminal returns immediately |
-p 80:80 |
Host port : container port |
--name |
A readable name instead of a random hash |
4.3 Viewing running containers
docker ps
docker ps -a
docker ps shows only running containers, -a adds stopped ones too — useful for finding a container that exited with an error right after startup.
4.4 Logs
docker logs my-nginx
docker logs -f my-nginx
-f (follow) works just like tail -f: convenient to keep open while diagnosing a problem in real time.
4.5 A shell inside the container
docker exec -it my-nginx sh
Opens an interactive shell inside an already-running container — check a config, inspect the network, look at files, without stopping the service.
4.6 Stopping and removing
docker stop my-nginx
docker rm my-nginx
stop sends a termination signal to the process inside the container, rm deletes the container itself along with its filesystem. The image it was created from stays on disk.
4.7 Managing images on disk
docker images
docker image prune
docker images lists downloaded images with their size, image prune clears out dangling images — the ones left untagged after a rebuild reused the same name.
5. Your first Dockerfile: a custom image
5.1 .dockerignore — the first file, not the Dockerfile
Before writing a Dockerfile, it's worth creating a .dockerignore: node_modules, .env, .git, *.log shouldn't end up inside the image. Without this file, the build context (everything sent to the daemon) balloons, and secrets from .env can accidentally end up baked into an image layer forever.
5.2 Dockerfile structure
The order of instructions matters — it's exactly what determines what gets cached and what gets rebuilt every single time:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]
5.3 Why package*.json is copied separately
If you copy the whole codebase with a single COPY . . before installing dependencies, any change in the code invalidates the npm ci layer's cache, and dependencies get reinstalled from scratch on every build. Splitting this into two steps means the npm ci layer is pulled from cache until package.json or package-lock.json themselves actually change.
5.4 Alpine or the full image
| Base image | Size | When to choose it |
|---|---|---|
node:20-alpine |
~130 MB | The production default |
node:20 |
~1.1 GB | You need system libraries Alpine doesn't have (musl vs glibc) |
5.5 CMD or ENTRYPOINT
| Instruction | Behavior |
|---|---|
CMD |
Default arguments; fully overridden by docker run image command |
ENTRYPOINT |
A fixed executable; arguments from docker run get appended to it instead of replacing it |
5.6 Building the image
docker build -t my-app:1.0 .
The same image can get a second tag without a rebuild — for example, docker build -t my-app:latest . in the same context creates another tag on the exact same layers.
5.7 Running your custom image
docker run -d -p 3000:3000 --name my-app my-app:1.0
6. Volumes: data outside the container
6.1 Why data disappears
A container's filesystem is ephemeral by design: docker rm deletes everything written inside it during its lifetime. For a stateless app that's not a problem, but for a database or any service holding state, it means data loss on every container update.
6.2 Bind mount
docker run -v /var/log/myapp:/app/logs my-app:1.0
A directory from the host is mounted directly inside the container, and changes show up on both sides in real time. Handy for configs you need to edit from outside, and for logs a separate monitoring system on the host reads afterward.
6.3 Named volume
docker volume create pgdata
docker run -v pgdata:/var/lib/postgresql/data postgres:16-alpine
A named volume is managed by Docker itself and physically lives under /var/lib/docker/volumes/. For databases this is usually the better choice over a bind mount: Docker handles the paths and permissions on its own, and a backup can be taken through a helper container with --volumes-from, without worrying about the host's exact directory structure.
| Parameter | Bind mount | Named volume |
|---|---|---|
| Location on the host | You specify it yourself | Managed by Docker |
| Typical use | Configs, logs, code during development | Database data, service state |
| Portability | Tied to the host's directory structure | The same command works on any server |
6.4 Managing volumes
docker volume ls
docker volume inspect pgdata
docker volume prune
prune removes volumes not used by any container — useful, but worth checking the list before running it: data deleted this way is gone for good.
7. Conclusion
Over the course of this article, we went from installing Docker Engine on a clean VPS to a custom-built image with data that survives a container restart. That's already enough to run a single application isolated from the rest of the system, without worrying about Node.js or Python version conflicts with a neighboring project.
An inconvenience that becomes obvious after your first few containers: every service is its own long docker run line with ports, environment variables, and volumes — easy to mix up or forget to recreate after a server update. Once an application isn't a single container but several — the app itself, a database, a cache — managing that set with individual commands gets hard and error-prone.
That's exactly the problem Docker Compose solves: the whole stack is described once in a docker-compose.yml file and comes up with a single command.
📚 Series navigation:
You're reading part 1 of 5, "Docker on a VPS: installation and your first container."
Next: Part 2. Docker Compose: multiple services together →
🚀 A VPS for your Docker workloads
Containers add CPU and disk overhead on top of the application itself: images, layers, logs, volumes for databases. Hostiserver gives you resources sized exactly for that.
🖥️ Dedicated Servers
- From $90/mo, full control over the hardware for heavy container workloads
- No shared resources: neighboring tenants don't affect your containers' performance
- 24/7 support: we'll help set up Docker and networking between services
💻 Cloud (VPS) Hosting
- From $19.95/mo, KVM isolation, dedicated vCPU and RAM
- NVMe disks: fast image layers and volumes for databases
- Perfect for your first Docker host: install it, spin up a container, try it on a real project
- Easy to scale: more RAM for new services, or a separate VPS per project
💬 Not sure which option you need?
💬 Reach out — we'll help you figure it out!
Frequently Asked Questions
- How much RAM does Docker need on a VPS?
Docker Engine itself takes up very little — up to 100-200 MB for the daemon and containerd. The bulk of consumption comes from the containers themselves: a small Node.js app fits in 256-512 MB, a database like PostgreSQL feels comfortable starting at 1 GB. For one application plus a database, a sensible starting point is a 2 GB RAM VPS, with margin for peak load and filesystem cache.
- Can you keep several projects with different Node.js versions on one VPS?
Yes, and it's one of the main reasons to move to Docker in the first place. Every container has its own isolated runtime, regardless of what's installed on the host. One container runs on
node:18-alpine, another right next to it runs onnode:20-alpine, and neither sees the other's environment.
- What's the practical difference between an image and a container?
An image is an immutable template on disk, similar to a class in programming. A container is a running instance of that template, with its own process and its own writable layer on top of the image's read-only layers. Deleting a container doesn't delete the image it was created from.
- The container exits immediately after starting — what do I check?
Start with
docker ps -ato see the exit code, thendocker logs container_name— in the vast majority of cases the cause is right there in the last lines of output: an app error on startup, a missing environment variable, a port already in use. A container exits when its main process exits, so it's almost always the application's own fault, not Docker's.
- Docker or a plain systemd service for a small project?
If a single application lives on the server and its dependencies don't conflict with anything, a systemd service is simpler and has fewer moving parts. Docker starts paying for itself the moment a second project shows up with different environment requirements, or you need reproducible builds, or you're planning to move the app to another server without the risk of "something's different on the new one."
- Is it safe to give a user Docker access without sudo?
Worth being honest here: membership in the
dockergroup is equivalent to root access on the host, since the daemon runs with root privileges and lets you mount any host directory inside a container. Only add people to this group who genuinely need full Docker administration on that specific server, not everyone who just needs to run a single container.
- Named volume or bind mount for a database?
A named volume is the typical choice for database data: Docker manages the storage path itself, and the command is the same regardless of the server. A bind mount makes sense when you need direct, predictable access to files from the host — for example, so a separate backup system on the host can read the database files directly, without going through an intermediate container.