Israel Casado

How to deploy your own applications on a VPS using Docker and Traefik

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.

Installation Steps

  1. 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. 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 system

    In 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 update

    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.

    Create a non-root user

    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
    SSH access configuration

    To 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 ssh
    Firewall (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 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.

    Final server state
    • Updated and secure
    • No root SSH access
    • Protected by firewall
    • Ready to install Docker and start deploying applications
  3. 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 installation

    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
    Run Docker without sudo

    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.

    Docker Compose installation

    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.

    Final verification

    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.

    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. 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 network

    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.

    Docker Compose of Traefik

    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.

    Providers: Docker as the source of truth

    Traefik 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 HTTPS

    Traefik 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. 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 structure

    Each application lives in its own directory and has its own docker-compose.yml. For example:

    /var/www/ └── mi-app-web/ └── docker-compose.yml

    This allows deploying, updating, or removing applications independently.

    Docker Compose of a web application

    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:

    • Detects the container automatically
    • Associates the domain indicated
    • Exposes the application by HTTPS
    • Generates and renews the SSL certificate
    Labels: the key of everything

    The 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 application

    Launching the application is as simple as executing:

    docker compose up -d

    In seconds:

    • Traefik detects the new service
    • The router is created dynamically
    • Let’s Encrypt issues the certificate
    • The application is accessible publicly
    Scalability and maintenance

    This 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. 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 practices
    Separate 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 networks

    Centralize 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 containers

    By default, Traefik does not expose any container (exposedbydefault=false). Only those services that really need public access should carry Traefik labels.

    Versionar los docker-compose

    Save 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 mistakes
    Run everything as root

    Although 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 protection

    The dashboard of Traefik is very useful, but should not be left exposed publicly without authentication or network restrictions.

    No control over logs

    Without 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 growth

    Although 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. 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 approach

    After 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
    Conclusion

    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.

Final of the guide

If you have any questions or want to share your experience, feel free to leave a comment or contact me directly. Happy deployment!