<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Traefik on Vezpi Lab</title>
        <link>https://blog.vezpi.com/en/tags/traefik/</link>
        <description>Recent content in Traefik on Vezpi Lab</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Thu, 01 Oct 2026 21:17:09 +0000</lastBuildDate><atom:link href="https://blog.vezpi.com/en/tags/traefik/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>How Do I Deploy Application Today</title>
            <link>https://blog.vezpi.com/en/post/how-i-deploy-application/</link>
            <pubDate>Sat, 31 Jan 2026 00:00:00 +0000</pubDate>
            <guid>https://blog.vezpi.com/en/post/how-i-deploy-application/</guid>
            <description>&lt;img src=&#34;https://blog.vezpi.com/en/post/how-i-deploy-application/cover-16.webp&#34; alt=&#34;Featured image of post How Do I Deploy Application Today&#34; /&gt;&lt;h2 id=&#34;intro&#34;&gt;Intro&#xA;&lt;/h2&gt;&lt;p&gt;In this post, I am not going to explain best practices for deploying applications. Instead, I want to document how I am currently deploying new applications in my homelab.&lt;/p&gt;&#xA;&lt;p&gt;Think of this article as a snapshot in time. This is how things really work today, knowing that in the near future I would like to move toward a more GitOps-oriented workflow.&lt;/p&gt;&#xA;&lt;p&gt;The method I use is fairly simple. I have tried to standardize it as much as possible, but it still involves quite a few manual steps.  I will also explain how I update applications, which is, in my opinion, the biggest weakness of this setup. As the number of applications keeps growing, keeping everything up to date requires more and more time.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;platform-overview&#34;&gt;Platform Overview&#xA;&lt;/h2&gt;&lt;p&gt;Before diving into the workflow, here is a quick overview of the main components involved.&lt;/p&gt;&#xA;&lt;h3 id=&#34;docker&#34;&gt;Docker&#xA;&lt;/h3&gt;&lt;p&gt;Docker is the foundation of my application stack. Whenever possible, I deploy applications as containers.&lt;/p&gt;&#xA;&lt;p&gt;I have been using Docker Compose for years. At the time, everything was running on a single physical server. Today, my setup is VM-based, and I could migrate to Docker Swarm, but I have chosen not to. It might make sense in some scenarios, but it is not aligned with where I want to go long term.&lt;/p&gt;&#xA;&lt;p&gt;For now, I still rely on a single VM to host all Docker applications. This VM is more or less a clone of my old physical server, just virtualized.&lt;/p&gt;&#xA;&lt;h3 id=&#34;proxmox-ve&#34;&gt;Proxmox VE&#xA;&lt;/h3&gt;&lt;p&gt;This VM is hosted on a Proxmox VE cluster, composed of three nodes and uses Ceph as a distributed storage backend.&lt;/p&gt;&#xA;&lt;p&gt;This gives me high availability and makes VM management much easier, even though the Docker workloads themselves are not highly available.&lt;/p&gt;&#xA;&lt;h3 id=&#34;traefik&#34;&gt;Traefik&#xA;&lt;/h3&gt;&lt;p&gt;Traefik runs directly on the Docker host and acts as the reverse proxy.&lt;/p&gt;&#xA;&lt;p&gt;It is responsible for routing the HTTPS traffic to the correct containers and for managing TLS certificates automatically using Let’s Encrypt. This keeps application-level configuration simple and centralized.&lt;/p&gt;&#xA;&lt;h3 id=&#34;opnsense&#34;&gt;OPNsense&#xA;&lt;/h3&gt;&lt;p&gt;OPNsense is my router, firewall and also acts as reverse proxy.&lt;/p&gt;&#xA;&lt;p&gt;Incoming HTTPS traffic is forwarded to Traefik using the Caddy plugin with Layer 4 rules. TLS is not terminated at the firewall level. It is passed through to Traefik, which handles certificate issuance and renewal.&lt;/p&gt;&#xA;&lt;h3 id=&#34;gitea&#34;&gt;Gitea&#xA;&lt;/h3&gt;&lt;p&gt;Gitea is a self-hosted Git repository, I have one instance running in my homelab.&lt;/p&gt;&#xA;&lt;p&gt;Inside Gitea, I have a private repository that contains all my Docker Compose configurations. Each application has its own folder, making the repository easy to navigate and maintain.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;deploy-new-application&#34;&gt;Deploy New Application&#xA;&lt;/h2&gt;&lt;p&gt;To standardize deployments, I use a &lt;code&gt;docker-compose.yml&lt;/code&gt; template that looks like this:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yml&#34; data-lang=&#34;yml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;services&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;NAME&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;IMAGE&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;container_name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;NAME&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;volumes&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#ae81ff&#34;&gt;/appli/data/NAME/:/&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;environment&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#ae81ff&#34;&gt;TZ=Europe/Paris&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;networks&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#ae81ff&#34;&gt;web&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;labels&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;traefik.enable=true&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;traefik.http.routers.NAME.rule=Host(`HOST.vezpi.com`)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;traefik.http.routers.NAME.entrypoints=https&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;traefik.http.routers.NAME.tls.certresolver=letsencrypt&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;traefik.http.services.NAME.loadbalancer.server.port=PORT&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;restart&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;always&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;networks&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;web&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;external&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Let me explain.&lt;/p&gt;&#xA;&lt;p&gt;For the image, depending on the application, the registry used could differ, but I still the Docker Hub by default. When I try a new application, I might use the &lt;code&gt;latest&lt;/code&gt; tag at first. Then if I choose to keep the it, I prefer to pin the current version instead of &lt;code&gt;latest&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I use volume binds for everything stateful. Every application got its own folder in the &lt;code&gt;/appli/data&lt;/code&gt; filesystem.&lt;/p&gt;&#xA;&lt;p&gt;When an application needs to be reachable with HTTPS, I link the container serving the requests in the &lt;code&gt;web&lt;/code&gt; network, which is managed by Traefik and I associate it labels. The &lt;code&gt;entrypoint&lt;/code&gt; and &lt;code&gt;certresolver&lt;/code&gt; is defined in my Traefik configuration. The URL defined in &lt;code&gt;Host()&lt;/code&gt; is the one which will be used to access the application. This needs to be the same as defined in the Layer4 route in the Caddy plugin of OPNsense.&lt;/p&gt;&#xA;&lt;p&gt;If several containers need to talk to each other, I add a &lt;code&gt;backend&lt;/code&gt; network which will be created when the stack will be deployed, dedicated for the application. This way, no ports need to be opened on the host.&lt;/p&gt;&#xA;&lt;h3 id=&#34;steps-to-deploy&#34;&gt;Steps to Deploy&#xA;&lt;/h3&gt;&lt;p&gt;Most of the work is done from VScode:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Create a new folder in that repository, with the application name.&lt;/li&gt;&#xA;&lt;li&gt;Copy the template above inside this folder.&lt;/li&gt;&#xA;&lt;li&gt;Adapt the template with the values given by the application documentation.&lt;/li&gt;&#xA;&lt;li&gt;Create a &lt;code&gt;.env&lt;/code&gt; file for secrets if needed. This file is ignored by &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Start the services directly from VS Code using the Docker extension.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Then in the OPNsense WebUI, I update 2 Layer4 routes for the Caddy plugin:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Depending if the application should be exposed on the internet or not, I have an &lt;em&gt;Internal&lt;/em&gt; and &lt;em&gt;External&lt;/em&gt; route. I add the URL given to Traefik in one of these.&lt;/li&gt;&#xA;&lt;li&gt;I also add this URL in another route to redirect the Let&amp;rsquo;s Encrypt HTTP challenge to Traefik.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Once complete, I test the URL. If everything is configured correctly, the application should be reachable over HTTPS.&lt;/p&gt;&#xA;&lt;p&gt;When everything works as expected, I commit the new application folder to the repository.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;update-application&#34;&gt;Update Application&#xA;&lt;/h2&gt;&lt;p&gt;Application updates are still entirely manual.&lt;/p&gt;&#xA;&lt;p&gt;I do not use automated tools like Watchtower for now. About once a month, I check for new versions by looking at Docker Hub, GitHub releases, or the application documentation.&lt;/p&gt;&#xA;&lt;p&gt;For each application I want to update, I review:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;New features&lt;/li&gt;&#xA;&lt;li&gt;Breaking changes&lt;/li&gt;&#xA;&lt;li&gt;Upgrade paths if required&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Most of the time, updates are straightforward:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Bump the image tag in the Docker Compose file&lt;/li&gt;&#xA;&lt;li&gt;Restart the stack.&lt;/li&gt;&#xA;&lt;li&gt;Verify that the containers restart properly&lt;/li&gt;&#xA;&lt;li&gt;Check Docker logs&lt;/li&gt;&#xA;&lt;li&gt;Test the application to detect regressions&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;If it works, I continue upgrading step by step until I reach the latest available version.&lt;/p&gt;&#xA;&lt;p&gt;If not, I debug until I fix the problem. Rollbacks are painful.&lt;/p&gt;&#xA;&lt;p&gt;Once the latest version is reached, I commit the changes to the repository.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;pros-and-cons&#34;&gt;Pros and Cons&#xA;&lt;/h2&gt;&lt;p&gt;What works well and what doesn&amp;rsquo;t?&lt;/p&gt;&#xA;&lt;h3 id=&#34;pros&#34;&gt;Pros&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;Simple model, one VM, one compose file per application.&lt;/li&gt;&#xA;&lt;li&gt;Easy to deploy, great to test an application.&lt;/li&gt;&#xA;&lt;li&gt;Central location for the configurations.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;cons&#34;&gt;Cons&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;Single Docker VM is a single point of failure.&lt;/li&gt;&#xA;&lt;li&gt;Manual updates don’t scale as the app count grows.&lt;/li&gt;&#xA;&lt;li&gt;Having to declare the URL on Caddy is boring.&lt;/li&gt;&#xA;&lt;li&gt;Hard to follow what is up, and what is not.&lt;/li&gt;&#xA;&lt;li&gt;Secrets in .env are convenient but basic.&lt;/li&gt;&#xA;&lt;li&gt;No fast way to rollback.&lt;/li&gt;&#xA;&lt;li&gt;Operations on the VM are critical.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&#xA;&lt;/h2&gt;&lt;p&gt;This setup works, and it has served me well so far. It is simple and intuitive. However, it is also very manual, especially when it comes to updates and long-term maintenance.&lt;/p&gt;&#xA;&lt;p&gt;As the number of applications grows, this approach clearly does not scale very well. That is one of the main reasons why I am looking toward GitOps and more declarative workflows for the future.&lt;/p&gt;&#xA;&lt;p&gt;For now, though, this is how I deploy applications in my homelab, and this post serves as a reference point for where I started.&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
