diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md
index d3f397e0f46c..3fbd21ee4f1d 100644
--- a/nixos/doc/manual/release-notes/rl-2611.section.md
+++ b/nixos/doc/manual/release-notes/rl-2611.section.md
@@ -28,6 +28,8 @@
- [FlapAlerted](https://github.com/Kioubit/FlapAlerted), detects BGP flapping events and provides statistics based on BGP update messages. Available as [services.flap-alerted](#opt-services.flap-alerted.enable).
+- [gocron](https://github.com/flohoss/gocron), a task scheduler with web interface. Available as [services.gocron](#opt-services.gocron.enable).
+
- [Unpackerr](https://unpackerr.zip), extracts downloads for Radarr, Sonarr, Lidarr, Readarr, and/or a Watch folder. Available as [services.unpackerr](#opt-services.unpackerr.enable).
- [Matrix Authentication Service](https://github.com/element-hq/matrix-authentication-service) is an OAuth2.0 and OpenID Connect provider for Matrix homeservers (such as Synapse). It replaces standard password authentication with modern OpenID Connect flows, and can delegate authentication to upstream OIDC providers. Available as [services.matrix-authentication-service](#opt-services.matrix-authentication-service.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 2535896d4790..692b4462c928 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1491,6 +1491,7 @@
./services/scheduling/atd.nix
./services/scheduling/cron.nix
./services/scheduling/fcron.nix
+ ./services/scheduling/gocron.nix
./services/scheduling/prefect.nix
./services/scheduling/scx-loader.nix
./services/scheduling/scx.nix
diff --git a/nixos/modules/services/scheduling/gocron.nix b/nixos/modules/services/scheduling/gocron.nix
new file mode 100644
index 000000000000..9916d9fd9da7
--- /dev/null
+++ b/nixos/modules/services/scheduling/gocron.nix
@@ -0,0 +1,173 @@
+{
+ options,
+ config,
+ lib,
+ pkgs,
+ ...
+}:
+let
+ cfg = config.services.gocron;
+ settingsFormat = pkgs.formats.yaml { };
+ gocronConf = settingsFormat.generate "gocron.yaml" cfg.settings;
+ defaultUser = "gocron";
+ defaultGroup = "gocron";
+ timeZone = config.time.timeZone;
+
+ hardeningOptions = lib.mkOption {
+ description = "Configuration for hardening the systemd service.";
+ type = lib.types.submodule {
+ options = {
+ ProtectHome = lib.mkOption {
+ description = ''
+ Whether to make the home directories inaccessible to the service.
+ See for more details.
+ '';
+ type = lib.types.either lib.types.str lib.types.bool;
+ default = true;
+ example = "read-only";
+ };
+ ProtectSystem = lib.mkOption {
+ description = ''
+ Whether to make several system directories inaccessible to the service.
+ See for more details.
+ '';
+ type = lib.types.either lib.types.str lib.types.bool;
+ default = true;
+ example = "full";
+ };
+ };
+ };
+ };
+in
+{
+
+ options.services.gocron = {
+ enable = lib.mkEnableOption "gocron, a task scheduler";
+
+ package = lib.mkOption {
+ default = pkgs.gocron;
+ defaultText = lib.literalExpression "pkgs.gocron";
+ type = lib.types.package;
+ description = ''
+ gocron package to use.
+ '';
+ };
+
+ openFirewall = lib.mkOption {
+ description = "Whether to open the firewall port to access the web ui.";
+ type = lib.types.bool;
+ default = false;
+ };
+
+ user = lib.mkOption {
+ description = "Unix User to run the server under";
+ type = lib.types.str;
+ default = defaultUser;
+ };
+
+ group = lib.mkOption {
+ description = "Unix Group to run the server under";
+ type = lib.types.str;
+ default = defaultGroup;
+ };
+
+ extraGroups = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ example = [ "backup" ];
+ description = ''
+ Additional groups for the systemd service.
+ '';
+ };
+
+ hardening = hardeningOptions;
+
+ settings = lib.mkOption {
+ # Setting this type allows for correct merging behavior
+ type = settingsFormat.type;
+ default = { };
+ description = ''
+ Configuration for gocron, see
+
+ for supported settings.
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = !lib.hasAttr "software" cfg.settings;
+ message = "Software installation configuration is only supported for traditional distros by upstream.";
+ }
+ ];
+
+ services.gocron.settings = {
+ time_zone = if timeZone != null then timeZone else lib.mkDefault "Etc/UTC";
+ server = {
+ address = lib.mkDefault "127.0.0.1";
+ port = lib.mkDefault 8156;
+ };
+ db.location = lib.mkDefault "/var/lib/gocron";
+ };
+
+ systemd.services.gocron = {
+ after = [ "network.target" ];
+ wantedBy = [ "multi-user.target" ];
+ serviceConfig = {
+ ExecStart = "${lib.getExe pkgs.gocron} --config '${gocronConf}'";
+ User = cfg.user;
+ Group = cfg.group;
+ DeviceAllow = "";
+ LockPersonality = true;
+ MemoryDenyWriteExecute = true;
+ MountAPIVFS = true;
+ NoNewPrivileges = true;
+ PrivateDevices = true;
+ PrivateMounts = true;
+ PrivateNetwork = lib.mkDefault false;
+ PrivateTmp = true;
+ PrivateUsers = true;
+ ProcSubset = "pid";
+ ProtectClock = true;
+ ProtectControlGroups = true;
+ ProtectHome = cfg.hardening.ProtectHome;
+ ProtectHostname = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ ProtectProc = "invisible";
+ ProtectSystem = cfg.hardening.ProtectSystem;
+ RemoveIPC = true;
+ RestrictAddressFamilies = [
+ "AF_INET"
+ "AF_INET6"
+ ];
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ UMask = "0077";
+ StateDirectory = lib.mkIf (cfg.settings.db.location == "/var/lib/gocron") "gocron";
+ SystemCallArchitectures = "native";
+ SystemCallErrorNumber = "EPERM";
+ SystemCallFilter = [
+ "@system-service"
+ ];
+ };
+ };
+
+ networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.settings.server.port ];
+
+ users.users.${cfg.user} = {
+ isSystemUser = true;
+ inherit (cfg) group;
+ };
+
+ users.groups.${cfg.group} = { };
+
+ meta = {
+ buildDocsInSandbox = true;
+ maintainers = with lib.maintainers; [ juliusfreudenberger ];
+ };
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 0bc01b90551f..71a18ed9e343 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -702,6 +702,7 @@ in
gobgpd = runTest ./gobgpd.nix;
gocd-agent = runTest ./gocd-agent.nix;
gocd-server = runTest ./gocd-server.nix;
+ gocron = runTest ./gocron.nix;
gocryptfs = runTest ./gocryptfs.nix;
gokapi = runTest ./gokapi.nix;
gollum = runTest ./gollum.nix;
diff --git a/nixos/tests/gocron.nix b/nixos/tests/gocron.nix
new file mode 100644
index 000000000000..2625dc474b53
--- /dev/null
+++ b/nixos/tests/gocron.nix
@@ -0,0 +1,58 @@
+{ pkgs, ... }:
+
+{
+ name = "gocron";
+ meta.maintainers = with pkgs.lib.maintainers; [ juliusfreudenberger ];
+
+ nodes.machine =
+ { pkgs, ... }:
+ {
+ environment.systemPackages = [ pkgs.jq ];
+ services.gocron = {
+ enable = true;
+ settings = {
+ jobs = [
+ {
+ name = "Test job";
+ disabled_cron = true;
+ commands = [
+ "echo 'Job runs successfully'"
+ ];
+ }
+ ];
+ };
+ };
+ };
+
+ testScript = ''
+ def gocron_is_up(_) -> bool:
+ status, _ = machine.execute("curl --fail http://localhost:8156")
+ return status == 0
+
+ def job_is_available() -> bool:
+ status, output = machine.execute("curl http://localhost:8156/api/jobs | jq '. | length'")
+ return status == 0 and int(output) == 1
+
+ def start_job():
+ machine.succeed("curl -X POST http://localhost:8156/api/jobs/Test%20job")
+
+ def job_ran_successfully() -> bool:
+ output = machine.succeed("curl http://localhost:8156/api/runs/Test%20job | jq '.[0].status_id, .[0].logs.[2].message'")
+ split_output = output.split('\n')
+ ran_successfully = int(split_output[0]) == 3
+ log_message_as_expected = "Job runs not successfully" in split_output[1]
+ return ran_successfully and log_message_as_expected
+
+ machine.wait_for_unit("gocron.service")
+ machine.wait_for_open_port(8156)
+ with machine.nested("Waiting for UI to work"):
+ retry(gocron_is_up)
+
+ with machine.nested("Test job"):
+ if not job_is_available():
+ exit(1)
+ start_job()
+ if not job_ran_successfully():
+ exit(1)
+ '';
+}
diff --git a/pkgs/by-name/go/gocron/package.nix b/pkgs/by-name/go/gocron/package.nix
new file mode 100644
index 000000000000..bc9ea1e8dd31
--- /dev/null
+++ b/pkgs/by-name/go/gocron/package.nix
@@ -0,0 +1,109 @@
+{
+ lib,
+ fetchFromGitHub,
+ nodejs,
+ fetchYarnDeps,
+ yarnConfigHook,
+ yarnBuildHook,
+ yarnInstallHook,
+ stdenv,
+ buildGoModule,
+ makeWrapper,
+ bash,
+ versionCheckHook,
+ nix-update-script,
+ nixosTests,
+}:
+
+buildGoModule (finalAttrs: {
+
+ pname = "gocron";
+ version = "0.9.14";
+ __structuredAttrs = true;
+
+ src = fetchFromGitHub {
+ owner = "flohoss";
+ repo = "gocron";
+ tag = "v${finalAttrs.version}";
+ hash = "sha256-LKjK5V+WrzTJlWPytafy8Ypva41RW4/12aSGaJj572I=";
+ };
+
+ gocron-web = stdenv.mkDerivation (finalAttrsWebassets: {
+ pname = "${finalAttrs.pname}-web";
+ src = "${finalAttrs.src}/web";
+ inherit (finalAttrs) version;
+
+ yarnOfflineCache = fetchYarnDeps {
+ yarnLock = finalAttrsWebassets.src + "/yarn.lock";
+ hash = "sha256-f0xnF9gd3c0KPrORPVkApyWPy+DazyzHeQu32wWybiw=";
+ };
+
+ nativeBuildInputs = [
+ yarnConfigHook
+ yarnBuildHook
+ yarnInstallHook
+ nodejs
+ ];
+
+ preBuild = ''
+ yarn types
+ '';
+
+ postBuild = ''
+ mv dist/ $out
+ '';
+
+ });
+
+ vendorHash = "sha256-VbmS9Fh0pr/dUB+pZBqKbi4bu6Do/3TRr9uI3TmGsOM=";
+
+ postPatch = ''
+ substituteInPlace handlers/web.go \
+ --replace-fail "web/assets" "${finalAttrs.gocron-web}/assets" \
+ --replace-fail "web/static" "${finalAttrs.gocron-web}/static" \
+ --replace-fail "web/index.html" "${finalAttrs.gocron-web}/index.html"
+ substituteInPlace main.go \
+ --replace-fail '"github.com/flohoss/gocron/internal/software"' "" \
+ --replace-fail "software.Install()" ""
+ '';
+
+ ldflags = [
+ "-s"
+ "-w"
+ "-X github.com/flohoss/gocron/internal/buildinfo.Version=${finalAttrs.version}"
+ ];
+
+ nativeBuildInputs = [
+ makeWrapper
+ ];
+
+ postInstall = ''
+ wrapProgram $out/bin/gocron --prefix PATH : ${
+ lib.makeBinPath [
+ bash
+ ]
+ }
+ '';
+
+ nativeInstallCheckInputs = [ versionCheckHook ];
+ doInstallCheck = true;
+
+ passthru.updateScript = nix-update-script {
+ extraArgs = [
+ "--subpackage"
+ "gocron-web"
+ ];
+ };
+ passthru.tests = nixosTests.gocron;
+
+ meta = {
+ description = "Task scheduler built with Go and Vue.js.";
+ homepage = "https://github.com/flohoss/gocron";
+ license = lib.licenses.mit;
+ mainProgram = "gocron";
+ maintainers = with lib.maintainers; [
+ juliusfreudenberger
+ ];
+ };
+
+})