Как запускать скрипт по расписанию без cron

На своём сервере расписание — это одна строка в crontab, и она работает годами. Привычка ломается в тот момент, когда сервера у вас больше нет: в контейнере нет crontab, нет терминала и некому запустить скрипт по будильнику. Хорошая новость в том, что расписание можно держать внутри самого кода — и часто это надёжнее системного cron. Рассказываем по шагам, как запускать скрипт по расписанию без cron и что предусмотреть, чтобы задача не сработала дважды.

Почему в контейнере нет cron

Контейнер — это один процесс, а не уменьшенная копия сервера. Внутри нет init-системы, которая поднимала бы службы, поэтому привычных демонов там просто не существует.

  • cron не установлен, и читать его некому. Системный планировщик — отдельная служба; в контейнер её обычно не кладут.
  • Терминала нет. Вы не подключитесь по SSH и не отредактируете crontab: наружу видны только код и панель.
  • Контейнер не должен ветвиться на процессы. Даже если поставить cron внутрь образа, он станет вторым процессом рядом со скриптом, и следить за ним будет некому.

Первое, что стоит принять: расписание переезжает внутрь приложения. Скрипт запускается один раз и работает постоянно, а решение «пора» принимает сам. Так устроены все фоновые сервисы — бот, воркер, парсер.

Расписание становится частью кода

Вы описываете не «запусти в 9:00», а сервис, который живёт всё время и держит внутри себя условие запуска. Правило простое: чем чаще задача, тем меньше нужна точность, и наоборот.

  • Раз в час или раз в десять минут. Хватит цикла с паузой.
  • Каждый день в 9:00 или по понедельникам. Нужна библиотека с правилами.
  • Раз в неделю или раз в месяц. Библиотека плюс отметка о последнем успешном запуске.

Внешний триггер тоже возможен: если у вас уже есть опубликованный сайт или API, добавьте маршрут /tasks/hourly, который делает работу, и позвольте внешнему cron-сервису открывать этот URL. URL публичный, поэтому защитите его секретным ключом в заголовке и читайте ключ из переменной окружения, а саму задачу делайте короткой — долгий ответ рискует отвалиться по таймауту на стороне вызывающего.

Способ 1. Цикл и пауза, когда время не критично

Самый простой вариант — вечный цикл. Скрипт делает работу, спит и повторяет:

import time
def job():
    print("Задача выполнена")
while True:
    job()
    time.sleep(3600)

Этого достаточно, когда важна частота, а не точная минута. Помните, что пауза считается от конца предыдущего запуска, а не от начала часа: если работа занимает время, момент срабатывания медленно уезжает. Для «примерно раз в час» это нормально, для отчёта ровно в 9:00 — уже нет.

Способ 2. Библиотека расписаний, когда нужно точное время

Для задачи «каждый день в 9:00» берут библиотеку. В Python это schedule для простых правил и APScheduler, когда правил много или хочется cron-подобных выражений; в Node.js ту же роль играет node-cron.

import schedule
import time
def job():
    print("Отчёт отправлен")
schedule.every().day.at("09:00").do(job)
while True:
    schedule.run_pending()
    time.sleep(30)

Обратите внимание: скрипт всё равно работает постоянно. Библиотека — это будильник внутри вашего процесса, а не внешняя служба, которая вас запустит. Хорошая практика для первой проверки: поставьте интервал в одну-две минуты, убедитесь, что задача срабатывает, и потом верните настоящее расписание.

Часовой пояс: считайте в UTC и не надейтесь на сервер

Часовой пояс сервера почти наверняка не ваш, поэтому «9 утра» легко превращается в другой час. Не полагайтесь на локальное время машины — задавайте зону явно:

from zoneinfo import ZoneInfo
from datetime import datetime
print(datetime.now(ZoneInfo("Europe/Moscow")))
print(datetime.now(ZoneInfo("UTC")))

Практичное правило: внутри кода считайте в UTC, а человеческий часовой пояс — только там, где нужно сравнить время с расписанием. У APScheduler есть аргумент timezone, у node-cron — соответствующая опция. Если сомневаетесь, логируйте текущее время при старте: сразу увидите, в какой зоне считает ваш скрипт.

Точка последнего запуска в постоянном хранилище

После перезапуска — из-за нового деплоя или падения — отсчёт начинается заново, и библиотека не помнит, что уже сделала. Из-за этого задача может сработать дважды подряд или пропустить свой момент, если перезапуск пришёлся ровно на него. Лечится отметкой:

import json
from pathlib import Path
marker = Path("/data/last_run.json")
if marker.exists():
    print(json.loads(marker.read_text())["at"])
else:
    print("первый запуск")

Пишите время последнего успешного запуска в файл в постоянной папке /data — она переживает обновление кода и перезапуск, а её содержимое видно в панели. При старте читайте отметку и решайте сами: запуск за сегодня уже сделан и его можно пропустить или момент был пропущен и работу надо выполнить сразу. Чем реже задача, тем важнее отметка: между двумя запусками раз в месяц проект наверняка переживёт деплой.

Логи и что происходит при сбое

Логируйте по строке на запуск — время и результат. Логи на Dockbox открыты в панели в реальном времени, поэтому и удачный запуск, и ошибка видны сразу, без подключения к серверу.

Если скрипт упадёт, платформа перезапустит контейнер автоматически — молчаливых падений можно не бояться, но помните: при перезапуске отсчёт сбросится, а отметка в /data уцелеет. Именно поэтому её и стоит хранить: она отделяет «процесс поднялся заново» от «работа действительно выполнена».

Тарифы и как работают контейнеры

Скрипт по расписанию — это фоновый сервис: он работает постоянно, его никто не будит ссылкой, поэтому ресурсы нужны всё время. На Dockbox контейнеры работают постоянно, без остановки по часам и без засыпания при простое — нет ни лимита по времени, ни «прогрева» после паузы.

  • Starter — 990 ₽/мес, первый месяц бесплатно: 1 vCPU, 2 ГБ RAM, 20 ГБ SSD, один проект. Скрипту с парой задач этого хватает с запасом.
  • Pro — 2 490 ₽/мес: 2 vCPU, 4 ГБ RAM, до 5 проектов — если задач станет больше или появится второй сервис.
  • Team — 4 990 ₽/мес: для команды и нескольких проектов.

Отдельных счетов за сборки и трафик нет, HTTPS бесплатный, код остаётся у вас. Начать можно с бесплатного первого месяца, чтобы увидеть, как проект собирается в контейнер и как в логах появляется первая строка о запуске.

Частые вопросы

Как запускать Python-скрипт каждый час?

Оберните работу в цикл с паузой на час — этого хватает, когда важна частота, а не точная минута. Если запуск должен попадать на начало часа, возьмите библиотеку расписаний: schedule для простых правил или APScheduler, когда их много. Скрипт всё равно работает непрерывно и публикуется как обычный проект.

Почему задача сработала не в то время?

Почти всегда дело в часовом поясе: часы сервера не обязаны совпадать с вашими, и «9 утра» считается в зоне машины. Задавайте зону явно через ZoneInfo и аргумент планировщика. Вторая причина — цикл с паузой: интервал считается от конца предыдущего запуска, поэтому момент срабатывания немного уезжает.

Что происходит с расписанием при перезапуске?

Отсчёт начинается заново: и цикл, и библиотека считают время от старта процесса и о прошлых запусках ничего не помнят. Поэтому задача может сработать дважды или пропустить момент, если перезапуск пришёлся ровно на него. Решение — хранить время последнего успешного запуска в постоянной папке /data и проверять его при старте.

schedule или APScheduler — что выбрать?

Для одного-двух простых правил вроде «каждый час» или «ежедневно в 9:00» проще schedule: он читается почти как предложение. APScheduler стоит брать, когда правил много, нужны cron-подобные выражения или параллельные задачи. В Node.js оба случая обычно закрывает node-cron.

Как узнать, что задача действительно отработала?

Логируйте строку на каждый запуск — время и результат. Логи видны в панели в реальном времени, поэтому и успех, и ошибка заметны сразу. Если процесс упадёт совсем, платформа перезапустит контейнер автоматически.