How to run the latest versions of your apps on a stable Linux release. The Dagger Way.

July 19, 2024

Jul 19, 2024

Lev Lazinskiy

Share
Share
Share
Share

I’ve been running Debian stable for a little while. I love it. But sometimes it’s so stable that it hurts. This is particularly true if you’re trying to also write code using the latest version of a programming language.

In my particular case, I want to create new content for my Hugo powered blog. But this is not possible because my blog was originally written with go 1.21 and the latest version of Hugo. Debian stable comes with go 1.19 and Hugo 0.111.3-1, which is an entire generation behind the latest hugo. Attempting to generate a new file causes Hugo to crash and burn for me.

There are many ways to fix this. I could use Debian backports to bring in some newer versions of the software. I could also just install go manually outside of the package system. I could even run a VM or container for working with Hugo. All of these approaches have pros and cons.

The reason why I am running Debian stable is because I want the entire OS to just work for many years without needing to fiddle with anything. In my experience every time you add a new package outside of the comfort and safety of the stable packages things inevitably go wrong.

Running a VM for Hugo feels like overkill, but a container would feel like a great solution. Especially if I could combine the power of containers with a real programming language and built-in CLI.

Enter Dagger

I attempted to use Dagger to solve this since it's already the core part of my blog publishing pipeline. I chose Dagger because it's purpose-built for creating artifacts without messing with local dependencies. If you think about it, creating a new post in Hugo is creating a new file artifact based on a content template and your site-specific configuration. Dagger allows me to easily execute an arbitrary version of Hugo in a sandboxed environment and return the file that I need.

I wrote a function called Write that accepts a title and returns the generated File that Hugo creates.

// create new post and return file
func (m *Publish) Write(ctx context.Context, title string) *File {
    file := fmt.Sprintf("%s.md", title)
    path := fmt.Sprintf("archive/%s", file)
    cmd := fmt.Sprintf("hugo new content %s", path)

    return m.base().
        WithMountedDirectory("/src", m.Src).
        WithWorkdir("/src").
        WithExec([]string{"sh", "-c", cmd}).
        Directory("content/archive").
        File(file

Dagger gives me a CLI for free every time I write a function. For example, I can call the Write function like this:

dagger call --src . write --title <TITLE> -o content/archive/<TITLE>.md
  • dagger call --src . write --title <TITLE> returns a file.

  • Adding -o content/archive/<TITLE>.md will write that file to my local computer at the specific path.

The only downside to this approach is that Dagger Functions can be quite verbose. This relatively simple function isn’t pretty to look at, and I need to type in <TITLE> twice.

I already have a hard enough time remembering the standard Hugo sequence hugo new content archive/<TITLE>.md where I always mess up the order of new and content.

Justfile to the rescue

I am obsessed with Justfile. It’s like Make without all the baggage of the first 20 years since the UNIX epoch. It allows you to write elegant wrappers for verbose CLI commands and the DX is a joy to use. It feels like the author listened to everyone’s complaints about Make for the last decade and wrote something that solves the pain.

Given this Just command:

# write new post with given title
[positional-arguments]
write title:dagger call --src . write --title $1 -o content/archive/$1.md

I can turn the verbose Dagger call into this little gem:

just write $title

It’s simpler than both the Dagger call and the native Hugo function. The entire sequence of operations now looks like this:

Now whenever I am ready to write a new blog post, I simply type in that Just command and I get the new file in my local directory using the latest version of Hugo and go without having to worry about messing with my system dependencies or risking the long term stability of Debian.

The approach described here works for any stack. In 2016, I wrote a post on my personal blog called “Working with chruby and nvm on Ubuntu”. That was me using two different version managers to manage the versions of two different programming languages on my local machine. I was practically begging the universe to give me some “works on my machine problems” to share with my team. Whether you’re trying to run the latest version of something to test an upgrade path, or the oldest version of something to support an important legacy customer (how is Java 6 still in production?), or if you’re like 2016 me trying to run several versions of several languages. Dagger provides you with an elegant solution to the version mismatch problem.

Want to discuss these scenarios and solutions with us more? Join the Dagger Discord to weigh in on all the community conversations!


I’ve been running Debian stable for a little while. I love it. But sometimes it’s so stable that it hurts. This is particularly true if you’re trying to also write code using the latest version of a programming language.

In my particular case, I want to create new content for my Hugo powered blog. But this is not possible because my blog was originally written with go 1.21 and the latest version of Hugo. Debian stable comes with go 1.19 and Hugo 0.111.3-1, which is an entire generation behind the latest hugo. Attempting to generate a new file causes Hugo to crash and burn for me.

There are many ways to fix this. I could use Debian backports to bring in some newer versions of the software. I could also just install go manually outside of the package system. I could even run a VM or container for working with Hugo. All of these approaches have pros and cons.

The reason why I am running Debian stable is because I want the entire OS to just work for many years without needing to fiddle with anything. In my experience every time you add a new package outside of the comfort and safety of the stable packages things inevitably go wrong.

Running a VM for Hugo feels like overkill, but a container would feel like a great solution. Especially if I could combine the power of containers with a real programming language and built-in CLI.

Enter Dagger

I attempted to use Dagger to solve this since it's already the core part of my blog publishing pipeline. I chose Dagger because it's purpose-built for creating artifacts without messing with local dependencies. If you think about it, creating a new post in Hugo is creating a new file artifact based on a content template and your site-specific configuration. Dagger allows me to easily execute an arbitrary version of Hugo in a sandboxed environment and return the file that I need.

I wrote a function called Write that accepts a title and returns the generated File that Hugo creates.

// create new post and return file
func (m *Publish) Write(ctx context.Context, title string) *File {
    file := fmt.Sprintf("%s.md", title)
    path := fmt.Sprintf("archive/%s", file)
    cmd := fmt.Sprintf("hugo new content %s", path)

    return m.base().
        WithMountedDirectory("/src", m.Src).
        WithWorkdir("/src").
        WithExec([]string{"sh", "-c", cmd}).
        Directory("content/archive").
        File(file

Dagger gives me a CLI for free every time I write a function. For example, I can call the Write function like this:

dagger call --src . write --title <TITLE> -o content/archive/<TITLE>.md
  • dagger call --src . write --title <TITLE> returns a file.

  • Adding -o content/archive/<TITLE>.md will write that file to my local computer at the specific path.

The only downside to this approach is that Dagger Functions can be quite verbose. This relatively simple function isn’t pretty to look at, and I need to type in <TITLE> twice.

I already have a hard enough time remembering the standard Hugo sequence hugo new content archive/<TITLE>.md where I always mess up the order of new and content.

Justfile to the rescue

I am obsessed with Justfile. It’s like Make without all the baggage of the first 20 years since the UNIX epoch. It allows you to write elegant wrappers for verbose CLI commands and the DX is a joy to use. It feels like the author listened to everyone’s complaints about Make for the last decade and wrote something that solves the pain.

Given this Just command:

# write new post with given title
[positional-arguments]
write title:dagger call --src . write --title $1 -o content/archive/$1.md

I can turn the verbose Dagger call into this little gem:

just write $title

It’s simpler than both the Dagger call and the native Hugo function. The entire sequence of operations now looks like this:

Now whenever I am ready to write a new blog post, I simply type in that Just command and I get the new file in my local directory using the latest version of Hugo and go without having to worry about messing with my system dependencies or risking the long term stability of Debian.

The approach described here works for any stack. In 2016, I wrote a post on my personal blog called “Working with chruby and nvm on Ubuntu”. That was me using two different version managers to manage the versions of two different programming languages on my local machine. I was practically begging the universe to give me some “works on my machine problems” to share with my team. Whether you’re trying to run the latest version of something to test an upgrade path, or the oldest version of something to support an important legacy customer (how is Java 6 still in production?), or if you’re like 2016 me trying to run several versions of several languages. Dagger provides you with an elegant solution to the version mismatch problem.

Want to discuss these scenarios and solutions with us more? Join the Dagger Discord to weigh in on all the community conversations!


I’ve been running Debian stable for a little while. I love it. But sometimes it’s so stable that it hurts. This is particularly true if you’re trying to also write code using the latest version of a programming language.

In my particular case, I want to create new content for my Hugo powered blog. But this is not possible because my blog was originally written with go 1.21 and the latest version of Hugo. Debian stable comes with go 1.19 and Hugo 0.111.3-1, which is an entire generation behind the latest hugo. Attempting to generate a new file causes Hugo to crash and burn for me.

There are many ways to fix this. I could use Debian backports to bring in some newer versions of the software. I could also just install go manually outside of the package system. I could even run a VM or container for working with Hugo. All of these approaches have pros and cons.

The reason why I am running Debian stable is because I want the entire OS to just work for many years without needing to fiddle with anything. In my experience every time you add a new package outside of the comfort and safety of the stable packages things inevitably go wrong.

Running a VM for Hugo feels like overkill, but a container would feel like a great solution. Especially if I could combine the power of containers with a real programming language and built-in CLI.

Enter Dagger

I attempted to use Dagger to solve this since it's already the core part of my blog publishing pipeline. I chose Dagger because it's purpose-built for creating artifacts without messing with local dependencies. If you think about it, creating a new post in Hugo is creating a new file artifact based on a content template and your site-specific configuration. Dagger allows me to easily execute an arbitrary version of Hugo in a sandboxed environment and return the file that I need.

I wrote a function called Write that accepts a title and returns the generated File that Hugo creates.

// create new post and return file
func (m *Publish) Write(ctx context.Context, title string) *File {
    file := fmt.Sprintf("%s.md", title)
    path := fmt.Sprintf("archive/%s", file)
    cmd := fmt.Sprintf("hugo new content %s", path)

    return m.base().
        WithMountedDirectory("/src", m.Src).
        WithWorkdir("/src").
        WithExec([]string{"sh", "-c", cmd}).
        Directory("content/archive").
        File(file

Dagger gives me a CLI for free every time I write a function. For example, I can call the Write function like this:

dagger call --src . write --title <TITLE> -o content/archive/<TITLE>.md
  • dagger call --src . write --title <TITLE> returns a file.

  • Adding -o content/archive/<TITLE>.md will write that file to my local computer at the specific path.

The only downside to this approach is that Dagger Functions can be quite verbose. This relatively simple function isn’t pretty to look at, and I need to type in <TITLE> twice.

I already have a hard enough time remembering the standard Hugo sequence hugo new content archive/<TITLE>.md where I always mess up the order of new and content.

Justfile to the rescue

I am obsessed with Justfile. It’s like Make without all the baggage of the first 20 years since the UNIX epoch. It allows you to write elegant wrappers for verbose CLI commands and the DX is a joy to use. It feels like the author listened to everyone’s complaints about Make for the last decade and wrote something that solves the pain.

Given this Just command:

# write new post with given title
[positional-arguments]
write title:dagger call --src . write --title $1 -o content/archive/$1.md

I can turn the verbose Dagger call into this little gem:

just write $title

It’s simpler than both the Dagger call and the native Hugo function. The entire sequence of operations now looks like this:

Now whenever I am ready to write a new blog post, I simply type in that Just command and I get the new file in my local directory using the latest version of Hugo and go without having to worry about messing with my system dependencies or risking the long term stability of Debian.

The approach described here works for any stack. In 2016, I wrote a post on my personal blog called “Working with chruby and nvm on Ubuntu”. That was me using two different version managers to manage the versions of two different programming languages on my local machine. I was practically begging the universe to give me some “works on my machine problems” to share with my team. Whether you’re trying to run the latest version of something to test an upgrade path, or the oldest version of something to support an important legacy customer (how is Java 6 still in production?), or if you’re like 2016 me trying to run several versions of several languages. Dagger provides you with an elegant solution to the version mismatch problem.

Want to discuss these scenarios and solutions with us more? Join the Dagger Discord to weigh in on all the community conversations!


Get Involved With the community

Discover what our community is doing, and join the conversation on Discord & GitHub to help shape the evolution of Dagger.

Subscribe to our newsletter

Get Involved With the community

Discover what our community is doing, and join the conversation on Discord & GitHub to help shape the evolution of Dagger.

Subscribe to our newsletter

Get Involved With the community

Discover what our community is doing, and join the conversation on Discord & GitHub to help shape the evolution of Dagger.

Subscribe to our newsletter