How to deploy your own applications on a VPS using Docker and Traefik
This guide walks you through installing and configuring a web server on a VPS from scratch, reducing traditional hosting costs while giving you full control over your deployment environment.
Installation Steps
- 1
Choosing the VPS and basic configuration
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.
The VPS I chose has the following configuration:- 2 vCPU
- 4 GB RAM
- 40 GB local disk
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.
Why Hetzner?- Excellent price-performance ratio
- Good CPU and disk performance
- Data centers in Europe with low latency
- Simple and effective control panel
- Ideal for personal projects, side projects, and small/medium production environments
Why is this configuration sufficient?Thanks to the use of Docker and Docker Compose, each application runs in its own container, allowing:
- Service isolation
- Resource optimization
- Easy scaling or adding new applications
Additionally, with Traefik as a reverse proxy, I can expose multiple applications and APIs under different domains or subdomains without increasing server complexity.
- 2
Access your VPS via SSH
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.
Operating systemIn my case, I opted for Ubuntu Server LTS, a common choice for servers for several reasons:
- Stability and long-term support
- Large community
- Excellent compatibility with Docker and modern tools
Nothing more to create the VPS, the first step was to access it via SSH as the root user to perform the initial configuration.
System updateThe first step is always to ensure the system is completely updated:
apt update && apt upgrade -yThis ensures we have the latest security patches and stable versions of the installed packages.
Create a non-root userFor 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 deploySSH access configurationTo improve server security, I made several adjustments to SSH access:
- Access via public/private key
- Disable direct root login
- Optionally, you can change the SSH port for greater security
After any change, it is necessary to restart the service:
systemctl restart sshFirewall (UFW)To limit access to only the necessary services, I configured the firewall with UFW (Uncomplicated Firewall).
First, I allowed the basic ports:
ufw allow OpenSSHAnd then enabled the firewall:
ufw enableWith this, the server only accepts traffic by SSH and by the HTTP/HTTPS ports, which will be used by Traefik.
Final server state- Updated and secure
- No root SSH access
- Protected by firewall
- Ready to install Docker and start deploying applications
- 3
Installing Docker and Docker Compose
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.
Docker installationAlthough 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:
2. Add the official Docker GPG key:apt install -y ca-certificates curl gnupg
3. Configure the repository: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
4. Install Docker Engine: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
5. Check the installation:apt update apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugindocker --version docker compose versionRun Docker without sudoTo avoid using sudo in each Docker command, I added my user to the docker group:
usermod -aG docker deployAfter closing the session and logging back in, it is possible to run Docker directly with the user.
Docker Compose installationIn this case, I used Docker Compose V2, which comes as an official plugin of Docker. This allows using the command:
docker composewithout needing additional binaries or depending on outdated versions.
Final verificationTo validate that everything was correctly installed, I launched a test container:
docker run hello-worldIf everything is well, Docker downloads the image and shows a confirmation message.
Base ready for Traefik and applications- Run multiple web applications
- Launch independent APIs
- Manage internal networks between containers
- Integrate a reverse proxy like Traefik
- 4
Setting up Traefik as a reverse proxy
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.
Why Traefik?I chose Traefik mainly because:
- Native integration with Docker
- Dynamic configuration based on labels
- Automatic HTTPS with Let’s Encrypt
- Simple management of multiple domains and subdomains
- Dashboard to monitor the proxy state
Shared Docker networkBefore 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 traefikThis network allows Traefik to route traffic to containers defined in different docker-compose.
Docker Compose of TraefikTraefik 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: trueWith this basic configuration, Traefik is already capable of detecting Docker containers and routing HTTP and HTTPS traffic.
Providers: Docker as the source of truthTraefik uses providers to discover services. In this case, the main provider is Docker:
- Traefik listens to Docker events
- Detects containers with specific labels
- Creates routes dynamically without restarts
This eliminates the need to edit configuration files each time a new application is added.
Preparation for automatic HTTPSTraefik allows managing SSL certificates automatically using Let’s Encrypt. For this, only the following is needed:
- Define an ACME resolver
- Configure an email of contact
- Persist the certificates in a volume
This allows that, when launching a new application with its domain, Traefik generates and renews the certificates automatically.
- 5
Exposing a real application with Traefik
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.
Basic project structureEach application lives in its own directory and has its own docker-compose.yml. For example:
/var/www/└── mi-app-web/└── docker-compose.ymlThis allows deploying, updating, or removing applications independently.
Docker Compose of a web applicationSuppose 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: trueWith just these labels, Traefik:
- Detects the container automatically
- Associates the domain indicated
- Exposes the application by HTTPS
- Generates and renews the SSL certificate
Labels: the key of everythingThe labels are the heart of the configuration with Traefik. In them, we define:
- Which containers are exposed (traefik.enable=true)
- Which domain or access rule is used
- By which entrypoint the traffic enters
- Which resolver of certificates to use
Everything is done without touching Traefik, simply declaring behavior from the application itself.
Deployment of the applicationLaunching the application is as simple as executing:
docker compose up -dIn seconds:
- Traefik detects the new service
- The router is created dynamically
- Let’s Encrypt issues the certificate
- The application is accessible publicly
Scalability and maintenanceThis approach allows:
- Add new applications without modifying the existing ones
- Use different domains and subdomains
- Restart or update services independently
- Maintain a clean and organized architecture
Each new project follows exactly the same pattern, simplifying maintenance enormously.
- 6
Best practices and common mistakes
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.
Good practicesSeparate responsibilities- Traefik must live in its own stack
- Each application must have its own docker-compose.yml
- Avoid mixing bases of data, proxies, and applications in the same stack
This facilitates updates, restarts, and debugging.
Use shared Docker networksCentralize the public exposure through a shared network (like traefik) allows:
- Maintain isolated internal networks
- Reduce the attack surface
- Control exactly which services are accessible from outside
Do not expose unnecessary containersBy default, Traefik does not expose any container (exposedbydefault=false). Only those services that really need public access should carry Traefik labels.
Versionar los docker-composeSave the docker-compose.yml in a repository (Git) allows:
- Reproduce the environment easily
- Have a historical record of changes
- Deploy the server from scratch in minutes
Persistence of data- Bases de datos siempre con volúmenes
- Certificados de Traefik persistidos
Avoid losing information when recreating containers.
Common mistakesRun everything as rootAlthough Docker allows it, it is not a good idea to work always as root. Using a dedicated user reduces risks and errors.
Expose the dashboard without protectionThe dashboard of Traefik is very useful, but should not be left exposed publicly without authentication or network restrictions.
No control over logsWithout a minimum strategy of logs:
- Errors pass unnoticed
- It is difficult to debug problems
- There is no visibility of the system
At least, it is worth revising the logs of Traefik and the applications periodically.
No plan for growthAlthough the VPS may be sufficient at the beginning, it is important:
- Monitor CPU and RAM consumption
- Know when to scale vertically
- Optimize images and services
- 7
Real-world usage in production
This entire infrastructure is not just an experiment or a laboratory. The very web you are reading now is deployed exactly in this way:
- Astro on the frontend
- .NET on the backend
- Independent Docker containers
- Traefik managing domains and HTTPS certificates
Each part lives in its own stack, but all coexist on the same VPS in an orderly, secure, and efficient manner.
Key advantages of this approachAfter using this architecture continuously, the main advantages are clear:
- Quick and reproducible deployments
- Complete isolation between applications
- Minimal configuration for new projects
- Automatic HTTPS without complications
- Clear scalability when needed
ConclusionSetting 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!