Files
Blog/content/post/how-I-deploy-application.md
Gitea Actions e2b1e02c0a
All checks were successful
Blog Deployment / Check-Rebuild (push) Successful in 7s
Blog Deployment / Build (push) Has been skipped
Blog Deployment / Deploy-Staging (push) Successful in 11s
Blog Deployment / Test-Staging (push) Successful in 3s
Blog Deployment / Merge (push) Successful in 8s
Blog Deployment / Deploy-Production (push) Successful in 12s
Blog Deployment / Test-Production (push) Successful in 3s
Blog Deployment / Clean (push) Has been skipped
Blog Deployment / Notify (push) Successful in 3s
Auto-update blog content from Obsidian: 2026-02-02 14:39:10
2026-02-02 14:39:10 +00:00

5.1 KiB
Raw Blame History

slug, title, description, date, draft, tags, categories
slug title description date draft tags categories
Template true

Intro

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.

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.

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.


Platform Overview

Before diving into the workflow, here is a quick overview of the main components involved.

Docker

Docker is the foundation of my application stack. Whenever possible, I deploy applications as containers.

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.

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.

Proxmox

All my VMs are hosted on a Proxmox cluster.

The cluster is composed of three nodes and uses Ceph as a distributed storage backend. This gives me high availability and makes VM management much easier, even though the Docker workloads themselves are not highly distributed.

Traefik

Traefik runs directly on the Docker host and acts as the reverse proxy.

It is responsible for routing HTTP and HTTPS traffic to the correct containers and for managing TLS certificates automatically using Lets Encrypt. This keeps application-level configuration simple and centralized.

OPNsense

@Explain briefly OPNsense

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.

Gitea

I host a Gitea server in my homelab.

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.

Deploy New Application

To standardize deployments, I use a docker-compose.yml template that looks like this:

services:
  NAME:
    image: IMAGE
    container_name: NAME
    volumes:
      - /appli/data/NAME/:/
    environment:
      - TZ=Europe/Paris
    networks:
      - web
    labels:
    - traefik.enable=true
    - traefik.http.routers.NAME.rule=Host(`HOST.vezpi.com`)
    - traefik.http.routers.NAME.entrypoints=https
    - traefik.http.routers.NAME.tls.certresolver=letsencrypt
    - traefik.http.services.NAME.loadbalancer.server.port=PORT
    restart: always

networks:
  web:
    external: true

Let me explain.

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 latest at start. Then if I choose to keep the application, I prefer to pin the version instead of latest.

Steps to deploy a new application: From VScode:

  • I create a new folder in that repository
  • I copy the template file inside this folder
  • I adapt the template with the values given by the application documentation
  • I try to avoid using the latest tag for the images
  • Eventually I create a .env file to store secrets which is ignored by the .gitignore of the repo
  • If volumes are needed, I use bind mounts on a specific FS on the server
  • I run the services directly from VScode using a Docker extension From OPNsense
  • In the Caddy plugin, I update 2 Layer4 routes:
    • Depending if the application should be exposed on the internet or not, I have an Internal or External route. I add the URL given to Traefik in one of these.
    • I also add this URL in another route to redirect the HTTP challenge to Traefik

Finally I test the URL and it should work! Once everything work as expected, I commit the new folder on the repo

Update Application

Updating my applications is still manual to me. I don't use tools like Watchtower for now. Every month or so, I check for new versions. I check on the Docker hub, GitHub or on the application documentation.

For each of the application I want to uppdate, I look for new features, breaking changes and try to bump them to the latest version.

Most of the time, updating an application is straightforward. I update the image tag and restart the docker compose stack. Then I verify if the application restart properly, check the docker logs and test the application to detect any regression.

If the tests are successful I continue to update until I reach the latest version available. Once reached, I commit the update in the repository.

Conclusion

Using Docker