This guide provides a step-by-step process on how to install and configure a web server on a VPS from scratch. Saving you all the costs of traditional hosting tools and gaining full control over your deployment environment.
To set up the entire infrastructure, I needed a solid, flexible foundation with a good price-performance ratio. After comparing several options, I chose Hetzner, a provider known for offering powerful resources at a very affordable cost.
It may seem like a modest configuration, but it is more than enough for my goal: running multiple web applications and APIs simultaneously using Docker containers.
Thanks to the use of Docker and Docker Compose, each application runs in its own container, allowing:
Additionally, with Traefik as a reverse proxy, I can expose multiple applications and APIs under different domains or subdomains without increasing server complexity.
Before starting to deploy containers and services, it is fundamental to prepare the server correctly. A good base avoids problems of security, performance, and maintenance in the future.
In my case, I opted for Ubuntu Server LTS, a common choice for servers for several reasons:
Nothing more to create the VPS, the first step was to access it via SSH as the root user to perform the initial configuration.
The first step is always to ensure the system is completely updated:
apt update && apt upgrade -y This ensures we have the latest security patches and stable versions of the installed packages.
For security, it is a good practice not to work directly with the root user. So I created a new user and assigned them sudo permissions:
adduser deploy To improve server security, I made several adjustments to SSH access:
After any change, it is necessary to restart the service:
systemctl restart ssh To limit access to only the necessary services, I configured the firewall with UFW (Uncomplicated Firewall).
First, I allowed the basic ports:
ufw allow OpenSSH And then enabled the firewall:
ufw enable With this, the server only accepts traffic by SSH and by the HTTP/HTTPS ports, which will be used by Traefik.
With the server already prepared and secured, the next step is to install Docker and Docker Compose, which will be the base for running all applications and services in an isolated and orderly manner.
Although Ubuntu includes Docker in its repositories, I prefer to install it from the official Docker repository to ensure I have a more current and stable version.
1. Install necessary packages:apt install -y ca-certificates curl gnupg 2. Add the official Docker GPG key: install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg 3. Configure the repository: 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" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null 4. Install Docker Engine: apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 5. Check the installation: docker --version
docker compose version To avoid using sudo in each Docker command, I added my user to the docker group:
usermod -aG docker deploy After closing the session and logging back in, it is possible to run Docker directly with the user.
In this case, I used Docker Compose V2, which comes as an official plugin of Docker. This allows using the command:
docker compose without needing additional binaries or depending on outdated versions.
To validate that everything was correctly installed, I launched a test container:
docker run hello-world If everything is well, Docker downloads the image and shows a confirmation message.
With Docker running correctly, the next key step is to introduce Traefik as a reverse proxy. Traefik becomes the single point of entry to all web applications and APIs that run on the server.
Its great advantage over other solutions is that it is designed for containerized environments, detecting services automatically through Docker.
I chose Traefik mainly because:
Before launching Traefik, I created a shared Docker network that will be used by Traefik and all applications that want to be exposed publicly:
docker network create traefik This network allows Traefik to route traffic to containers defined in different docker-compose.
Traefik runs as a container, normally in its own stack. A simplified docker-compose.yml would be:
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--api.dashboard=true"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- traefik
networks:
traefik:
external: true With this basic configuration, Traefik is already capable of detecting Docker containers and routing HTTP and HTTPS traffic.
Traefik uses providers to discover services. In this case, the main provider is Docker:
This eliminates the need to edit configuration files each time a new application is added.
Traefik allows managing SSL certificates automatically using Let’s Encrypt. For this, only the following is needed:
This allows that, when launching a new application with its domain, Traefik generates and renews the certificates automatically.
Once Traefik is running as a reverse proxy, the next step is to put it to work: exposing a real web application using Docker Compose and Traefik labels.
In this point, everything starts to fit and the power of this architecture is appreciated.
Each application lives in its own directory and has its own docker-compose.yml. For example:
This allows deploying, updating, or removing applications independently.
Suppose a simple web application (like an API or a frontend). The docker-compose.yml could be something like this:
services:
app:
image: nginx:alpine
container_name: mi_app_web
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.miapp.rule=Host(`miapp.midominio.com`)"
- "traefik.http.routers.miapp.entrypoints=websecure"
- "traefik.http.routers.miapp.tls.certresolver=letsencrypt"
networks:
- traefik
networks:
traefik:
external: true With just these labels, Traefik:
The labels are the heart of the configuration with Traefik. In them, we define:
Everything is done without touching Traefik, simply declaring behavior from the application itself.
Launching the application is as simple as executing:
docker compose up -d In seconds:
This approach allows:
Each new project follows exactly the same pattern, simplifying maintenance enormously.
After setting up the infrastructure and deploying several applications, there is a series of good practices that mark the difference between a system that simply functions and one that is maintainable, secure, and scalable over time.
This facilitates updates, restarts, and debugging.
Centralize the public exposure through a shared network (like traefik) allows:
By default, Traefik does not expose any container (exposedbydefault=false). Only those services that really need public access should carry Traefik labels.
Save the docker-compose.yml in a repository (Git) allows:
Avoid losing information when recreating containers.
Although Docker allows it, it is not a good idea to work always as root. Using a dedicated user reduces risks and errors.
The dashboard of Traefik is very useful, but should not be left exposed publicly without authentication or network restrictions.
Without a minimum strategy of logs:
At least, it is worth revising the logs of Traefik and the applications periodically.
Although the VPS may be sufficient at the beginning, it is important:
This entire infrastructure is not just an experiment or a laboratory. The very web you are reading now is deployed exactly in this way:
Each part lives in its own stack, but all coexist on the same VPS in an orderly, secure, and efficient manner.
After using this architecture continuously, the main advantages are clear:
Setting up a modern web server today does not require complex infrastructures or large investments. With a well-chosen VPS, Docker, Traefik, and good organization, it is possible to build a solid, flexible, and scalable platform.
This architecture allows me to focus on what is important: developing applications, knowing that the base on which they run is stable, maintainable, and easy to evolve.
If you are thinking about setting up your own server to host several applications and APIs, this approach is a good recommendation.
If you have any questions or want to share your experience, feel free to leave a comment or contact me directly. Happy deployment!