Running Scheduled Console Apps with .Net Core, Cron and Docker

Published | 4 min read

I am a big fan of .Net Core at the moment. Mostly because I can do development natively on my Mac and the fact I can run them in Docker containers (did I mention I love Docker?).

With .Net Core you can build any app you can currently build in vanilla .Net, such as console apps, Web APIs, MVC apps. As you know, console apps generally come in 2 flavours, run all the time and run on a schedule. Typically the scheduled console apps are just hooked up to Windows Task Scheduler and forgotten about (until they break).

Given .Net Core is supposed to be cross-platform, what is the cross-platform equivalent for Windows Task Scheduler? I had a look at building in a timer loop or using a framework such as FluentScheduler but this just felt like adding bulk to an otherwise small console application.

Then a friend reminded me that Docker contains are running Linux. Of course, how simple we can just use Cron!

All Hail Cron

For the uninitiated, Cron is Linux’s version of Windows Task Scheduler. It doesn’t have a UI but is controlled by files placed in /etc/cron.d/.

As I found out, running cron successfully in Docker with a .Net Core console app comes with a few gotchas!

Gotcha #1: No environment variables

For my first attempt I created a schedule file which simply ran my dotnet core console app using cron on a schedule.

My file looked like this:

* * * * * root dotnet /app/dotnet-cron.dll >> /var/log/cron.log 2>&1

#* * * * * *
#| | | | | |
#| | | | | +-- Year (range: 1900-3000)
#| | | | +---- Day of the Week (range: 1-7, 1 standing for Monday)
#| | | +------ Month of the Year (range: 1-12)
#| | +-------- Day of the Month (range: 1-31)
#| +---------- Hour (range: 0-23)
#+------------ Minute (range: 0-59)
# Cron requires a blank space at the end of the file so leave this here.

Which brings us on to Gotcha #1. Cron runs in its own environment with all the environment variables stripped out.

Typically any environment changes in a Docker container are controlled using environment variables. With a Microsoft extension, you can use AddEnvironmentVariables to override the settings in your appsettings.json file.

You are probably used to seeing environment variables specified in your docker compose file like this:


As we are dealing with compiled images the only way to cope with this is to export your environment variables at runtime and have them set by cron before your application runs.

My approach was to use printenv and write them all to a file prepending them with the export command. Which brings us on to Gotcha #2.

Gotcha #2: Invalid environment variables

So let us see what happens when we export one of our environment variables:

bash: export: `Database:ConnectionString=Server=': not a valid identifier

By what I can only assume is black magic, Docker manages to set environment variables even though they have invalid characters. In this case, it is the colon (:) that is causing the issue. Luckily .Net Core accepts a double underscore instead which does work with the Linux export function.

While looking into this I did find something interesting. If you use double underscore the environment variables are available when the CMD line is run at the end of your Dockerfile. However, if you use colons these are actually set after your container has been set up.

Docker Compose also throws in another curve ball into the mix by setting an affinity:container environment variable which not only contains a colon also has a double equals!

In the end, I used the following script to output all my environment variables into a separate script. I had to use sed to wrap the values in quotes otherwise this caused yet another headache.


echo '#!/bin/bash' > /app/
printenv | sed '/^affinity:container/ d' | sed -r 's/^([a-zA-Z_]+[a-zA-Z0-9_-]*)=(.*)$/export \1="\2"/g' >> /app/
chmod +x /app/

My Dockerfile then runs this script before running cron: I then just tell to run the following script which sets the environment variables and runs my console application.


# Set environment variables copied from container
source /app/;

# Run your dotnet console app
dotnet /app/dotnet-cron.dll

Gotcha #3: Cross-platform considerations

The last gotcha only occurred when I got other people to build and run my Docker image on their machines. It worked fine the fellow Mac users but for those on Windows, the resulting image would never run the console on schedule.

I had seen this problem before. Those with any experience working with Linux and Windows will know what I am talking about, carriage returns. Windows line endings are different from Linux in that they consist of not just a new line (\n) but a carriage return (\r) as well.

So in the Dockerfile I stripped out the carriage returns from all of the Linux based files using sed. The command looks like this:

sed -i 's/\r//'

Final Result

The final result can be seen here on my GitHub page:

If this has been useful or you know of a better way of doing any of the above then please comment below.


Stack vs Heap Memory - What are the differences?

Stack vs Heap Memory - What are the differences?

  • 30 November 2022
In modern programming languages such as C# or Java, we tend to take memory management for granted. Gone are the days when we need to call malloc to request enough memory for our variables. Luckily a lot of that is done for us by the runtimes so we do...
Code Katas: Can They Make You A Better Developer?

Code Katas: Can They Make You A Better Developer?

  • 21 November 2022
They say “practice makes perfect”, although I much prefer “practice makes improvement”. Either way, how do you practice being a programmer? If you are already working as a software developer then you will be getting some practice from working on larg...
Git Flow vs GitHub Flow

Git Flow vs GitHub Flow

  • 10 November 2022
Losing code that you have spent hours writing can be painful, which is why we use version control (or source control) to store our code and manage changes. Version control is even more important if you are working in a team, without it code, changes ...
I Posted on YouTube Consistently for 1 Month. This is What Happened!

I Posted on YouTube Consistently for 1 Month. This is What Happened!

  • 02 November 2022
As part of my creative sabbatical, I have been posting a new software development video on my YouTube channel every Monday and Friday. It takes a long time to grow on YouTube, and I knew this going in but I have been pleasantly surprised with my grow...
Bitwise Operators and WHY we use them

Bitwise Operators and WHY we use them

  • 26 October 2022
Bitwise operators are one of those concepts that a lot of programmers don’t understand. These are not used a great deal anymore so you can get away with not knowing them but they can still come in handy for a number of different scenarios. If you end...
8 Data Structures you NEED to Know

8 Data Structures you NEED to Know

  • 26 October 2022
You can get pretty far in programming without understanding Data Structures, but eventually, you are going to need to know them, understand how they work and when to use them. What is a data structure? A da...
Binary Numbers Explained for Programmers

Binary Numbers Explained for Programmers

  • 21 October 2022
Everyone knows that computers run on ones and zeros. This is because CPUs are made up of billions of transistors, which are basically just on-off switches. Any code you write needs to be processed by a computer and therefore has to be converted to b...
Beginners Guide to Programming

Beginners Guide to Programming

  • 12 October 2022
A lot of my articles are aimed at intermediate to advanced developers, but as part of my creative sabbatical, I am working on creating content for those just starting out. So in this post I will be covering some of the many questions that beginner pr...