How to schedule a Python script on a VPS
Run your Python script every 15 minutes on an Ubuntu VPS. Set it up once, check a scheduled result, find errors, and turn the schedule off.
In this guide
Your script can run on a schedule while your laptop is off. On an Ubuntu VPS—a computer you rent online—use a systemd timer to start it at set times. A small companion file, called a service, tells Ubuntu which Python file to run.
Choose your own script or the optional folder report below, then follow one setup. You’ll run it once, schedule it every 15 minutes, check a result from the schedule, and learn how to turn it off. No domain, monitoring account or extra scheduler is needed.
Before you start
This walkthrough uses Ubuntu 24.04, Python 3.12 and systemd 255. Have a VPS with that OS, its IP address, and an administrator login that can use sudo. Keep your existing server if it fits. If you are still choosing hosting, check whether you need a VPS before buying one.
Need a VPS and don’t have one yet? Follow our Ubuntu VPS setup guide to create one and make your first connection, then return here to choose your file.
Use a server where the account dn-report, folders /opt/deploy-notes-report and /var/lib/deploy-notes-report, and deploy-notes-report service/timer names are unused. These commands create a separate setup; they are not an upgrade procedure for one already installed.
Choose your own file
Use one Python file that already works and finishes by itself, without keyboard input, required command-line arguments, private credentials or extra input files. It must be safe to repeat. Open its source on your computer so you can copy it in step 2; you can skip the example entirely.
The setup allows ordinary output files such as result.txt in a dedicated working folder. If your script needs extra Python packages, have its existing, reviewed requirements.txt ready too. Multi-file projects and secret setup need additional preparation outside this walkthrough. A script that runs forever belongs in the 24/7 guide.
Or try a small folder report
The optional example counts files and their total size in a folder. It gives you a simple result to check and can help track a folder used for incoming files. It writes report.json, replacing the previous report each run. It does not delete files, scan subfolders or make a backup.
Save your choice for step 2. Both paths use the same filename, job.py, and the same schedule.
1. Connect and prepare Ubuntu
Already connected after our VPS setup guide? Keep that terminal open and skip the SSH login below. If you need to connect again, reuse your full SSH command, including -i and your chosen key file, instead of the generic command below.
Otherwise, on your computer, open Terminal on macOS/Linux or PowerShell on Windows. Replace the two uppercase placeholders with your server login and IP address:
ssh YOUR_LOGIN@YOUR_SERVER_IP
On the first connection, SSH asks whether to trust the server. Compare its fingerprint with the one from your provider’s authenticated console before accepting. For an ED25519 key, run ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub in that browser console and compare the SHA256:… value and key type. If they differ, or SSH shows another key type, stop and verify with the provider. Use your configured SSH key or enter the login password; passwords do not appear as you type.
The remaining commands run in this server terminal. Check the environment:
cat /etc/os-release
python3 --version
systemd --version
ps -p 1 -o comm=
timedatectl status
Look for Ubuntu 24.04, Python 3.12, systemd 255, and systemd as the first process. If Python is missing, the next command installs it. The clock should say System clock synchronized: yes. If not, try sudo timedatectl set-ntp true, wait briefly and check again. Resolve a clock that stays unsynchronized with your provider before setting a schedule.
Install Python’s environment tool and the nano text editor, then create your source folder:
sudo apt-get update &&
sudo apt-get install --no-install-recommends python3 python3-venv nano &&
mkdir -p ~/deploy-notes-report-src &&
cd ~/deploy-notes-report-src
sudo requests administrator permission. The && between commands stops the block if a step fails. Fix that error before continuing; earlier successful steps are not undone.
2. Save and install your chosen file
Run nano job.py. Paste your own file, or the complete example below. To save in nano, press Ctrl+O, then Enter, then Ctrl+X to return to the terminal. Use the same save sequence for the other files in this guide.
Optional example only:
import argparse
import json
from pathlib import Path
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input-dir", type=Path, default=Path("inbox"))
parser.add_argument("--output", type=Path, default=Path("report.json"))
args = parser.parse_args()
files = [path for path in args.input_dir.iterdir()
if not path.is_symlink() and path.is_file()]
report = {"files": len(files),
"total_bytes": sum(path.stat().st_size for path in files)}
# Keep temporary output beside the destination for the final rename.
temporary = args.output.with_name(args.output.name + ".tmp")
temporary.write_text(json.dumps(report) + "\n", encoding="utf-8")
temporary.replace(args.output)
print(f"report written: {args.output}", flush=True)
if __name__ == "__main__":
main()
Now install the file you chose. Run this from ~/deploy-notes-report-src:
sudo useradd --system --user-group --home-dir /nonexistent --no-create-home --shell /usr/sbin/nologin dn-report &&
sudo install -d -o root -g root -m 0755 /opt/deploy-notes-report &&
sudo install -o root -g root -m 0644 job.py /opt/deploy-notes-report/job.py &&
sudo python3 -m venv /opt/deploy-notes-report/.venv &&
sudo install -d -o dn-report -g dn-report -m 0750 /var/lib/deploy-notes-report
This creates a limited account for the job, copies its code, and gives it a virtual environment: a folder with its own Python interpreter path and packages. Output goes in /var/lib/deploy-notes-report; the script cannot change its installed code. If a line fails, fix it and resume from that line down. Do not rerun a successful useradd or reuse an unfamiliar existing account.
Only if your own file needs extra packages: run nano requirements.txt in this source folder, paste your existing reviewed requirements, save, then run sudo /opt/deploy-notes-report/.venv/bin/python -m pip install -r requirements.txt. Resolve any install error before continuing. The example needs no packages.
Only for the folder example: create a small, known input:
sudo -u dn-report /bin/sh -c 'mkdir -p /var/lib/deploy-notes-report/inbox && printf "hello\n" > /var/lib/deploy-notes-report/inbox/example.txt'
That file contains six bytes, including its newline. The report should count one file and six bytes. For real use, the input folder must be readable by dn-report; keep it quiet during a scan. Files changing mid-scan can invalidate the count or fail the job.
3. Run it once through the service
Still in the source folder, run nano deploy-notes-report.service, paste this, and save:
[Unit]
Description=Run a scheduled Python job
[Service]
Type=oneshot
User=dn-report
Group=dn-report
WorkingDirectory=/var/lib/deploy-notes-report
ExecStart=/opt/deploy-notes-report/.venv/bin/python -u /opt/deploy-notes-report/job.py
TimeoutStartSec=10min
StateDirectory=deploy-notes-report
StateDirectoryMode=0750
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
UMask=0027
Type=oneshot means “run the job and wait for it to finish.” ExecStart points to your installed file; you do not need to change it for the example. WorkingDirectory is the folder where relative paths such as result.txt begin. The remaining restrictions keep the job away from home folders and prevent most filesystem writes outside its output folder.
This setup stops a job that exceeds ten minutes. That is a chosen limit, not a speed claim. Use it for a task that comfortably finishes within that time; increase TimeoutStartSec before installation if your known workload needs longer.
Install the instructions, check them, and run the selected job:
sudo install -m 0644 deploy-notes-report.service /etc/systemd/system/deploy-notes-report.service &&
sudo systemd-analyze verify --recursive-errors=yes /etc/systemd/system/deploy-notes-report.service &&
sudo systemctl daemon-reload &&
sudo systemctl start deploy-notes-report.service &&
sudo systemctl show deploy-notes-report.service -p Result -p ExecMainStatus
Expect Result=success and ExecMainStatus=0. The service normally becomes inactive after finishing; that is correct. If the block fails, read the error log before setting a schedule.
Check the actual result too. For your own script, open its expected output—for example, sudo cat /var/lib/deploy-notes-report/result.txt—and confirm the contents. If it prints its result instead, use sudo journalctl -u deploy-notes-report.service -n 20 --no-pager. A successful exit alone does not prove useful work happened.
For the example, sudo cat /var/lib/deploy-notes-report/report.json should show {"files": 1, "total_bytes": 6}. You now have a working job; the timer only adds when it runs.
4. Set the schedule
Run nano deploy-notes-report.timer in the same source folder, paste this, and save:
[Unit]
Description=Run the Python job every 15 minutes
[Timer]
OnCalendar=*-*-* *:00,15,30,45:00 UTC
AccuracySec=1s
Persistent=true
Unit=deploy-notes-report.service
[Install]
WantedBy=timers.target
This starts the service at minutes 00, 15, 30 and 45 of each hour in UTC, independently of your local timezone. Check the next four times:
systemd-analyze calendar --iterations=4 '*-*-* *:00,15,30,45:00 UTC'
AccuracySec=1s requests a small scheduling window, not an exact-second guarantee. Persistent=true allows one catch-up run after an inactive period, such as server downtime; it does not replay every missed interval. If catching up would be harmful for your task, change it to false before installing. If a previous run is still active, this timer leaves it running instead of starting another copy.
Enable the timer, which will also start at boot:
sudo install -m 0644 deploy-notes-report.timer /etc/systemd/system/deploy-notes-report.timer &&
sudo systemd-analyze verify --recursive-errors=yes /etc/systemd/system/deploy-notes-report.service /etc/systemd/system/deploy-notes-report.timer &&
sudo systemctl daemon-reload &&
sudo systemctl enable --now deploy-notes-report.timer &&
sudo systemctl list-timers --all deploy-notes-report.timer
Look for a future time in NEXT. You can close your server terminal; the VPS owns the schedule now.
5. Check a scheduled result
Wait until after the displayed NEXT time and allow the job to finish. If you disconnected, reconnect with the same SSH command from step 1. If you followed our VPS setup guide, keep -i and your chosen key file in that full command. Then check:
sudo systemctl list-timers --all deploy-notes-report.timer
sudo systemctl show deploy-notes-report.service -p Result -p ExecMainStatus
sudo journalctl -u deploy-notes-report.service -n 20 --no-pager
Look for a recent LAST time, a successful exit, and log entries from that scheduled run, not just your earlier manual test. For the example, run sudo stat /var/lib/deploy-notes-report/report.json and sudo cat /var/lib/deploy-notes-report/report.json: the Modify time should have advanced and the count should still match the input. For your own script, check the equivalent output or printed result from this run.
If those agree, you’ve verified a scheduled execution. A timer waiting for its next run, by itself, is not proof of success.
6. Turn the schedule off
To stop future runs without deleting your code or results:
sudo systemctl disable --now deploy-notes-report.timer
sudo systemctl is-enabled deploy-notes-report.timer
Expect disabled; this check returns a nonzero status for a disabled timer. A job that already started may still be finishing. Let it finish, or use sudo systemctl stop deploy-notes-report.service only if interruption is safe. To resume, run sudo systemctl enable --now deploy-notes-report.timer and check list-timers again. With persistence enabled, resuming may trigger a catch-up run.
Read errors
Use sudo journalctl -u deploy-notes-report.service -n 50 --no-pager to see the program’s printed output and errors. A Python traceback ends with the error to investigate.
ModuleNotFoundError: install the script’s reviewed requirements into/opt/deploy-notes-report/.venv, using the command in step 2. Installing into your login user’s Python is a different environment.PermissionErroror a missing file: check the path. The job runs asdn-report, starts in/var/lib/deploy-notes-report, and cannot read your home folder. For the example, confirm you createdinbox/example.txt; for your own program, keep output in its working folder.- A timeout or no new output: check the log and service result. An old output file can survive a failed run. A script that waits for keyboard input or never exits does not fit this schedule.
Keep the timer disabled while changing code or inputs, and let any active job finish first. If you need to fix the Python code, edit nano ~/deploy-notes-report-src/job.py, save it, then copy that revision into the installed location and test it:
cd ~/deploy-notes-report-src &&
sudo install -o root -g root -m 0644 job.py /opt/deploy-notes-report/job.py &&
sudo systemctl start deploy-notes-report.service
For a package or input fix alone, no code copy is needed: run sudo systemctl start deploy-notes-report.service. In either case, verify the actual output again before resuming the timer with the command in step 6. You do not need to recreate the service or environment.
Change the time later
Disable the timer as in step 6. Edit its source with nano ~/deploy-notes-report-src/deploy-notes-report.timer. For example, OnCalendar=*-*-* 09:00:00 UTC means daily at 09:00 UTC. Save, then check that expression:
systemd-analyze calendar --iterations=2 '*-*-* 09:00:00 UTC'
Expect the next two daily 09:00 UTC times. If you chose another schedule, replace the quoted expression with yours. Return to the source folder with cd ~/deploy-notes-report-src and repeat only the timer install/verify/enable block in step 4. Do not recreate the account, Python environment or service. Check the new NEXT time and a scheduled result.
Use one scheduler for this job. A second cron entry or a direct Python launch can still create duplicate work; a systemd timer is not a lock against those. For actions such as sending messages or making payments, the program itself must prevent unsafe repeats. Server downtime and job errors can still prevent a useful result at a particular time.
Sources and useful links
- Ubuntu’s Python version table — check the Python series supplied with your Ubuntu release.
- Python 3.12 virtual environments — understand the separate interpreter and package folder used here.
- systemd services, version 255 and execution settings — look up oneshot jobs, timeouts, working folders and file-access restrictions.
- systemd timers and calendar expressions — change the schedule or understand missed runs.
- cPanel Cron Jobs — a different route if you already have supported shared hosting; these VPS service files do not go into a cPanel cron field.