Self-hosted service
flohoss/gocron avatar
flohoss/gocron

gocron: a YAML-driven scheduler with a web UI on top of cron

A task scheduler built with Go and Vue.js that allows users to specify recurring jobs via a simple YAML configuration file. The scheduler reads job definitions, executes commands at specified times using cron expressions, and passes in environment variables for each job.

617 stars23 forksGoMIT

At a glance

What is it?
gocron is a self-hosted task scheduler written in Go with a Vue front end, where recurring jobs are declared in a YAML file, run on standard 5-field cron expressions and keep their run history in SQLite. It fits a homelab operator who wants cron plus a dashboard, and it is heavier than a crontab for anyone who only needs cron.
Who is it for?
gocron fits an operator who wants recurring jobs declared in a version-controlled file, triggered and inspected from a browser, with run history kept in SQLite, and who is comfortable running a container on a small always-on host. It is the wrong choice for a single machine where a crontab and a log file do the same job, and the wrong choice for a multi-tenant service, since the config is one file and the UI is a single admin surface.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One YAML file describes the schedule, the commands and the environment

gocron is a self-hosted task scheduler built with Go and Vue.js. The README summarises the design in one sentence: define recurring jobs in a YAML file, run them on cron schedules, pass per-job environment variables, and manage everything through a web UI backed by SQLite.

The configuration file is the centre of the system. GoCron reads ./config/config.yaml by default, and the path can be overridden with --config. On first boot the file is created automatically from a commented reference config, config/config.example.yaml, so a first run gives you a working file with example jobs rather than an empty directory and a startup error.

The structure is three levels. A time_zone setting applies to all schedules and is reapplied on config reload, alongside log_level with debug, info, warn and error as the choices, and delete_runs_after_days, where 0 means keep run history forever. A db block gives the location, absolute or relative to the config file, and the name of the SQLite file. A server block sets the listen address and port, and carries the proxy and security settings.

Then there is job_defaults, and the README describes it as values inherited by every job that does not override them. The fields are cron, timeout, retries, envs, pre_commands and post_commands.

Finally the jobs list, where each entry has a name, its own cron expression, and a list of commands that run in sequence.

The whole file is a declaration of intent, and that is what makes it reviewable. A job definition is a diff, not a runtime action, which is a meaningfully different way to operate scheduled work from editing a crontab on a server.

Job defaults and per-job overrides, with env expansion in commands

The inheritance model is the part worth understanding before you write a config, because it changes how you add a job.

A job without its own cron inherits the default one. A job with its own cron overrides it, and the README's example makes that explicit with a comment on the Nightly Backup entry saying it overrides job_defaults.cron. The same pattern applies to timeout, to retries and to environment variables.

The environment variable handling is the detail with real consequences. Each job gets its own env, with ${VAR} expansion in commands, and job-specific values override defaults with the same key. The example sets a default of SLEEP_TIME to 5 under job_defaults, and the job then sets RESTIC_REPOSITORY to /backups/nightly in its own envs block. The key is the override, not the union: a job that sets a key with the same name replaces the default rather than adding to it.

The other per-job fields are about failure behaviour. The timeout aborts a single command after the given duration, with 30s and 5m given as the format examples. The retries field retries each failing command up to N additional times. There is also disable_fail_fast, set to false in the example, with the description that it stops on the first failing command by default, so setting it to true continues past a failure.

Then there are the pre and post commands, run once before and once after the job's own commands, and the README's example uses them for a Starting backup and a Backup finished message. That is a useful pattern for anything where you want consistent logging or a marker file regardless of which commands the job runs.

The design goal here is that a job declares what it needs, not how the environment is assembled. For a homelab with a dozen jobs, that is the difference between a config you can copy between machines and a pile of shell scripts that each set their own variables.

Running it: one container command and a config volume

The quick start is a single container invocation, because GoCron creates a default config with example jobs on first start if none exists:

sh
docker run -it --rm \
  --name gocron \
  -p 8156:8156 \
  -v ./config/:/app/config/ \
  ghcr.io/flohoss/gocron:latest

The volume mount is the part that matters. Mounting ./config/ over /app/config/ puts your config file where the process expects it, which is also what makes the live reload work.

The equivalent Docker Compose service is short enough to read in one go:

yml
services:
  gocron:
    image: ghcr.io/flohoss/gocron:latest
    restart: always
    ports:
      - '8156:8156'
    volumes:
      - ./config/:/app/config/

Once it is up, you open http://localhost:8156 to view your jobs, trigger runs manually and inspect logs. The README is explicit that editing ./config/config.yaml is picked up automatically with no restart needed, and there is a whole configuration reloads section in the table of contents for the details.

For people who do not want a container, tagged GitHub releases include downloadable Linux binaries, and you can run ./gocron_<version>_linux_<arch> --version to inspect the embedded version metadata.

There is also a first-class NixOS path. GoCron is packaged in nixpkgs with a NixOS module, and the README shows the service configuration, which sets time_zone to UTC, a job_defaults cron of 0 3 * * 0 and a Hello World job whose commands list is a single echo. The module also has an openFirewall option, and the README points at search.nixos.org for the package and the module options.

The web UI, SQLite state, live updates and health check callbacks

The feature list is where gocron separates itself from a cron wrapper, and several items are more than they sound.

The web UI lets you view jobs, trigger runs, read logs and use a sandboxed terminal, with dark and light mode. Live updates mean job runs stream over Server-Sent Events without polling, which matters for a UI that is meant to feel like it is showing you something happening.

SQLite state is the persistence layer: run history and logs persist in a local SQLite database. The delete_runs_after_days setting controls how long that history is kept, and 0 means keep it forever. The go.mod shows the driver choice, glebarez/go-sqlite, which is a pure Go SQLite implementation rather than a cgo binding, and there is a sqlc.yml in the repository root, so the SQL is generated rather than hand-written.

Health checks are the feature to pay closest attention to, and they are described as HTTP callbacks at job start, end and failure for alerting. That is the integration point with the rest of your monitoring: gocron tells your endpoint when a job starts, finishes or fails, and the alerting logic lives on your side.

Security features are optional rather than assumed. Reverse proxy support means you can trust X-Forwarded-For from configured proxies for accurate client IPs, and the config comments show the trusted_proxies list shape. Rate limiting and CORS are both off or wide open by default, with a rate_limit block showing enabled, a rate of 20 requests per second per client IP and a burst of 40, and a cors block whose allow_origins defaults to *. A comment in the config notes that the trusted_proxies and cors settings are commented out in the example.

Single sign-on is optional too, with OIDC login for the UI and the API, and the go.mod carries coreos/go-oidc for it.

The Go side: cron, echo, viper and a file watcher

The dependency list in go.mod describes the architecture more precisely than the README does.

Scheduling is robfig/cron/v3, the standard cron expression library for Go, which is why the README can promise standard 5-field cron expressions without implementing a parser. Configuration is viper, with go-viper/mapstructure for decoding the YAML into structs, and the dependency list also includes go-playground/validator and universal-translator for request and config validation.

The web layer is labstack/echo/v5 with huma/v2 alongside it, and the latter is an OpenAPI-first framework, which suggests the API is described by a schema rather than hand-written route documentation. Authentication for OIDC is coreos/go-oidc with go-jose for the token handling, and golang.org/x/oauth2 for the flow.

The reload behaviour is fsnotify, a filesystem watcher, which is the standard way to notice a config file change and act on it. The README notes that the time zone is applied on config reload, which is the kind of detail that only matters if you change it and expect the change to take effect.

The repository layout is organised by concern: internal/ for the implementation, pkg/ for library packages, handlers/ for HTTP handlers, services/ for the service layer, web/ for the Vue front end, config/ for the configuration, docker/ for the container builds, and main.go at the root. The top-level compose.yml is used for development, with a separate compose.e2e.yml for end-to-end tests.

The go.mod requires Go 1.27.2, and the compose file pins the same version for the build image, along with Node 26 for the front end and a specific air version for hot reload. The presence of air, of a healthcheck against the scheduler's own /health endpoint and of a pre-command that runs sqlc generate tells you the development loop is set up to regenerate the database code and rebuild the binary on change.

Where gocron is the wrong tool

The first wrong-tool case is a single server with five jobs. Cron, a log file and a notification script do the same job with a fraction of the moving parts, and gocron brings a web server, a database, a front end and a container image to a problem that did not have those requirements. The value of the web UI scales with the number of jobs, not with the elegance of the approach.

The second is a multi-tenant or per-user scheduling service. The configuration is a single YAML file and the UI is a single admin surface, with a config file rather than a database as the source of truth for the schedule. A service where each customer has their own jobs needs a different model, and retrofitting it into a file-based design is a rewrite.

The third is a deployment where the scheduler and the jobs cannot share a container. This is a genuine constraint that the README does not spell out. The health check callbacks are HTTP requests to a URL, which is fine when everything is in one container, and awkward when the job runs on a host that cannot reach the scheduler. Similarly, the sandboxed terminal in the UI, the pre-installed backup tools and the rest of the environment all assume the jobs run inside the image, where restic, borgbackup and rclone are available.

The fourth is a team that wants scheduling as a platform feature with a real contract. There is no indication in the README of a supported upgrade path, a long-term support policy or a compatibility statement between versions, and the config file format is not described as versioned, so a format change across an upgrade is a thing to check rather than assume.

The fifth is anyone who treats the sandboxed terminal and the per-job environment variables as a security boundary. The jobs run shell commands with the environment you give them, and the UI offers a terminal, so the deployment's real security boundary is the container and the people who can reach port 8156. Rate limiting and CORS are off by default, and OIDC is optional.

Editorial conclusion

gocron fits an operator who wants recurring jobs declared in a version-controlled file, triggered and inspected from a browser, with run history kept in SQLite, and who is comfortable running a container on a small always-on host. It is the wrong choice for a single machine where a crontab and a log file do the same job, and the wrong choice for a multi-tenant service, since the config is one file and the UI is a single admin surface. Before deploying, decide whether the scheduler and the jobs run in the same container, because that determines whether a job can reach the scheduler's own port, and check the health check callback design before you rely on it for alerting.

Frequently asked questions

How do I run gocron for the first time?

Run the container with the config directory mounted, and GoCron creates a default config with example jobs on first start if none exists, then you open http://localhost:8156 to view the jobs, trigger runs and read logs.

What can I put in a gocron job definition?

Each job has a name, an optional cron expression, timeout, retries and disable_fail_fast, its own envs, pre_commands and post_commands, and a commands list whose entries run in sequence, with ${VAR} expansion in the commands.

Do I need to restart gocron after changing config.yaml?

No, the README says changes to ./config/config.yaml are picked up automatically with no restart needed, and the configuration reloads section covers the details, including that the time zone is applied on reload.

How does gocron integrate with alerting?

Through health checks, which are HTTP callbacks fired at job start, at job end and on failure, so the alerting logic stays in your own monitoring system rather than inside the scheduler.

Official sources

  1. flohoss/gocron on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/flohoss-gocron.svg)](https://hysenlabs.com/projects/flohoss-gocron)

Was this useful?