How to run a Python script 24/7 on a VPS

Keep your Python script running after you close your laptop. Set it up once on an Ubuntu VPS, check its output, and learn how to stop and restart it.

Beginner guide14 min read
In this guide

Your Python script can keep running after you close your laptop. Put it on an Ubuntu VPS—a computer you rent online—and let systemd, Ubuntu’s service manager, handle background execution, logs and retries after a crash.

Use a simple script you already have, or the optional website checker below. You’ll follow one setup path, see the selected script work, then check that it continues after you disconnect. This guide targets Ubuntu 24.04, Python 3.12 and systemd 255. It fits a program that stays running without keyboard input; a program that finishes and needs another run later belongs on a schedule.

Have a VPS already?

Keep it if it runs Ubuntu 24.04 and gives you administrator access. Have its IP address, login name and SSH key or password ready, then choose your file.

If you need a server, look for an ordinary Linux VPS with Ubuntu 24.04, administrator SSH access and permission to run persistent processes. Choose memory and disk space for your script’s needs; a GPU or managed database is not needed for the example here. Check the provider’s current price and billing terms before creating anything.

For the steps from choosing a server to your first login, follow our Ubuntu VPS setup guide, then return here to choose your file. It uses DigitalOcean; another provider with the same capabilities is fine. You do not need to buy a domain for the VPS.

Choose your file

Use your own script

This path fits one Python file that already works, keeps running and needs no keyboard input or command-line arguments. Open it on your computer so you can copy its contents in step 2. You won’t need to install the example first.

Check these limits before starting:

  • Extra Python packages are supported if you already have a reviewed requirements.txt listing their versions. We’ll install them before the first run. A package that needs additional Ubuntu libraries needs its own setup too.
  • This walkthrough does not supply private API keys, passwords or supporting input files. If your program needs those, prepare their application-specific configuration first; don’t paste secrets into the readable code or service files below. Values set only in your terminal won’t automatically reach the service.
  • Relative output names, such as result.txt, will write to /var/lib/deploy-notes-worker. Writing beside the code using __file__, or using a path on your laptop, won’t work with this setup. Your program must support the writable folder. It also needs a visible result—a printed message or output file you can check.

If that describes your script, go straight to step 1.

Or use this website checker

No script ready? This example checks one public page roughly every minute and records its response. It gives you a recent result and a log of checks made while you’re away. It does not send alerts or guarantee that your website is available. Use your own page or one you have permission to check.

You’ll paste this code in step 2 and replace https://YOUR_WEBSITE with the full address of that page, keeping the quotation marks. No extra packages or accounts are needed.

import argparse
import math
import signal
from datetime import datetime, timezone
from pathlib import Path
from threading import Event
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import Request, urlopen

PAGE_URL = "https://YOUR_WEBSITE"


def check_page(url):
    request = Request(url, headers={"User-Agent": "DeployNotesWebsiteCheck/1.0"})
    try:
        with urlopen(request, timeout=10) as response:
            return f"OK HTTP {response.status}"
    except HTTPError as error:
        result = f"CHECK FAILED HTTP {error.code}"
        error.close()
        return result
    except OSError as error:
        return f"CHECK FAILED {error}"


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--url", default=PAGE_URL)
    parser.add_argument("--state-dir", type=Path, default=Path("."))
    parser.add_argument("--interval", type=float, default=60.0)
    args = parser.parse_args()
    if args.url == "https://YOUR_WEBSITE":
        parser.error("Replace https://YOUR_WEBSITE in app.py with your page address")
    address = urlsplit(args.url)
    if address.scheme not in ("http", "https") or not address.hostname:
        parser.error("--url needs a full http:// or https:// address")
    if address.username is not None or address.password is not None:
        parser.error("Use a public URL without a username or password")
    if not math.isfinite(args.interval) or args.interval <= 0:
        parser.error("--interval must be a positive, finite number")

    stopping = Event()
    signal.signal(signal.SIGTERM, lambda *_: stopping.set())
    signal.signal(signal.SIGINT, lambda *_: stopping.set())

    # Save the latest result here; Ubuntu keeps the printed check history.
    args.state_dir.mkdir(parents=True, exist_ok=True)
    latest = args.state_dir / "latest-check.txt"
    while not stopping.is_set():
        result = check_page(args.url)
        timestamp = datetime.now(timezone.utc).isoformat()
        line = f"{timestamp} {result}"
        latest.write_text(line + "\n", encoding="utf-8")
        print(line, flush=True)
        stopping.wait(args.interval)
    print("checker stopped", flush=True)


if __name__ == "__main__":
    main()

1. Connect and prepare Ubuntu

Use your existing VPS if you can safely add this service there; a fresh test VPS is another option. The account dn-worker, folders /opt/deploy-notes-worker and /var/lib/deploy-notes-worker, and service deploy-notes-worker.service must not belong to another setup.

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 PowerShell on Windows, or Terminal on macOS or Linux. Replace the two placeholders with your server login and IP address:

ssh YOUR_USER@YOUR_SERVER_IP

SSH gives you a terminal on the server. If your provider supplied a command with a key file or different port, use that command. The login needs sudo access, or may be the provider’s initial root administrator login. The script itself will use a separate, restricted account.

On the first connection, SSH shows a fingerprint that identifies the server. If your hosting account doesn’t display it, open the provider’s browser-based server console—for example, the Droplet Console—and run ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub there. Compare its SHA256:... value with the terminal’s ED25519 fingerprint. Accept only if they match. If another key type is shown, or the values differ, verify it with your provider before continuing.

Return to your computer’s SSH terminal. If asked for a password, type it and press Enter. Nothing appears while you type; that’s normal.

Once the server’s welcome message appears, run the remaining commands in this connected window. Check the environment:

cat /etc/os-release
systemd --version
ps -p 1 -o comm=

Look for Ubuntu VERSION_ID="24.04", systemd 255 and a final line of systemd. Stop here if these don’t match; a container or another operating system may need different instructions.

Install Python and the small text editor nano, then make a folder for your editable files:

sudo apt-get update &&
sudo apt-get install --no-install-recommends python3 python3-venv nano &&
mkdir -p ~/deploy-notes-worker-src &&
cd ~/deploy-notes-worker-src

sudo runs a command with administrator permission. Enter your password if asked, and Y if the installer asks to continue. The && means “continue only if this command succeeded.” If a block fails, fix that step before continuing; earlier steps aren’t undone.

Run python3 --version; it should show Python 3.12.x.

2. Put your chosen file on the server

In the connected terminal, open a file named app.py:

nano app.py

Paste either your own script’s contents or the checker above, keeping indentation. This copies the text into the server file; your original local file is unchanged. For the checker, replace the address on the PAGE_URL line now. Don’t paste both programs.

Save with Ctrl+O, press Enter to confirm app.py, then Ctrl+X to close nano. On a Mac, use Control, not Command.

3. Install it and get your first result

We’ll use dn-worker only to run the script. Its installed code will be protected from changes by that account; its output folder will be writable.

sudo useradd --system --user-group --home-dir /nonexistent --no-create-home --shell /usr/sbin/nologin dn-worker &&
sudo install -d -o root -g root -m 0755 /opt/deploy-notes-worker &&
sudo install -o root -g root -m 0644 app.py /opt/deploy-notes-worker/app.py &&
sudo python3 -m venv /opt/deploy-notes-worker/.venv &&
sudo install -d -o dn-worker -g dn-worker -m 0750 /var/lib/deploy-notes-worker

The virtual environment, .venv, gives this project its own Python packages without changing Ubuntu’s. We create it once on the server; don’t copy an environment from your laptop. These commands may finish silently. If a line fails, fix its cause and rerun from that line downward. Don’t repeat successful account creation: useradd stops when that account exists. If you didn’t create dn-worker in this attempt, don’t reuse it without checking what already owns it.

Only if your script needs packages: open nano requirements.txt, paste your existing reviewed package list and save it with the same shortcuts. Install it using this project’s Python:

sudo /opt/deploy-notes-worker/.venv/bin/python -m pip install -r requirements.txt

Skip that for the checker or another standard-library-only script. Install only packages you trust; stop if installation fails.

Now run the installed file as its own account, from the output folder:

sudo -u dn-worker /bin/sh -c 'umask 027 && cd /var/lib/deploy-notes-worker && exec /opt/deploy-notes-worker/.venv/bin/python -u /opt/deploy-notes-worker/app.py'

This is a foreground check: the terminal is still attached to the program. -u makes printed messages appear promptly. Keep the full command on one line.

For your script, look for its expected message or result. Relative output files are under /var/lib/deploy-notes-worker; after stopping the foreground run, you can inspect a text result with sudo cat /var/lib/deploy-notes-worker/result.txt, replacing result.txt with its actual name.

For the checker, expect a UTC timestamp followed by OK HTTP 200 or another successful response code. CHECK FAILED includes the reason: check the address if you see 404, or the connection if it times out. The script follows normal redirects; a blocked automated request doesn’t prove the site is down.

Wait until you’ve seen real progress, then press Ctrl+C to return to the prompt. The checker prints checker stopped; an in-progress request can take a moment to finish. Your own program may print KeyboardInterrupt, which is normal for this manual stop.

The chosen file now works on the VPS. Next, Ubuntu will run it without keeping this terminal open.

4. Give Ubuntu the startup instructions

A service is a program managed in the background. From your editable-files folder, create its instructions:

cd ~/deploy-notes-worker-src &&
nano deploy-notes-worker.service

Paste the entire file below. It is the same for your own script and the checker; no website address or example-specific arguments belong here.

[Unit]
Description=Deploy Notes Python script
StartLimitIntervalSec=60
StartLimitBurst=5

[Service]
Type=exec
User=dn-worker
Group=dn-worker
WorkingDirectory=/var/lib/deploy-notes-worker
ExecStart=/opt/deploy-notes-worker/.venv/bin/python -u /opt/deploy-notes-worker/app.py
Restart=on-failure
RestartSec=5
StateDirectory=deploy-notes-worker
StateDirectoryMode=0750
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
UMask=0027

[Install]
WantedBy=multi-user.target

Save with Ctrl+O, Enter, Ctrl+X. ExecStart is the command to run. WorkingDirectory keeps relative output in the writable folder. Restart=on-failure retries a crashed program after five seconds; repeated quick failures can stop automatic retries.

The remaining access settings block home folders and writes to most of the server. The code stays under /opt; output stays under /var/lib. A successful foreground run doesn’t yet prove that your own script works under these service restrictions, so check its actual output next.

5. Start it, disconnect and check again

Install the instructions, check them, and enable the service:

sudo install -m 0644 deploy-notes-worker.service /etc/systemd/system/deploy-notes-worker.service &&
sudo systemd-analyze verify --recursive-errors=yes /etc/systemd/system/deploy-notes-worker.service &&
sudo systemctl daemon-reload &&
sudo systemctl enable --now deploy-notes-worker.service

The last command starts it now and enables startup when the VPS boots. Check the result:

sudo systemctl is-enabled deploy-notes-worker.service
sudo systemctl is-active deploy-notes-worker.service
sudo journalctl -u deploy-notes-worker.service -n 20 --no-pager

Expect enabled and active. The last command shows the program’s recent messages—its logs. Check your script’s real output too: active means a process is running, not that it is doing useful work.

For the checker, run sudo cat /var/lib/deploy-notes-worker/latest-check.txt. Wait a little over a minute and repeat it: the timestamp should advance. It waits 60 seconds after each request, so slow requests lengthen the gap. UTC timestamps may differ from your local clock.

At the command prompt, press Ctrl+D to disconnect. Wait long enough for your script to do another piece of work—two minutes for the checker—then reconnect with the SSH command from step 1. If you followed our VPS setup guide, keep -i and your chosen key file in that full command. Repeat the status, logs and output checks. New results from while you were disconnected confirm that your laptop is no longer keeping the script alive.

Startup after reboot is configured, not demonstrated by this disconnection check. Systemd cannot keep working during a VPS outage or fix a program’s bugs.

6. Stop and resume it

To pause the program:

sudo systemctl stop deploy-notes-worker.service &&
sudo systemctl is-active deploy-notes-worker.service

Expect inactive. That check returns a nonzero status because the program is stopped. A deliberate stop doesn’t trigger a failure restart, but the next reboot will start an enabled service.

To resume now, run sudo systemctl start deploy-notes-worker.service, then repeat the output check from step 5. To stop it and prevent startup at boot, use sudo systemctl disable --now deploy-notes-worker.service. Your files remain. Restore both with sudo systemctl enable --now deploy-notes-worker.service.

Logs, updates and troubleshooting

You can leave the setup running here. Use this section when you need to inspect or change it.

Read more logs

sudo journalctl -u deploy-notes-worker.service -n 50 --no-pager

For live messages, replace -n 50 --no-pager with -f. Ctrl+C stops the log viewer, not the service.

For checker failures recorded today:

sudo journalctl -u deploy-notes-worker.service --since today --grep="CHECK FAILED" --no-pager

An empty result means no matching failures were found in retained logs. Retention depends on the server’s settings; this isn’t a permanent uptime report. latest-check.txt holds only the most recent result.

Update your script

Edit ~/deploy-notes-worker-src/app.py with nano, save it, then copy the updated file and restart:

sudo install -o root -g root -m 0644 ~/deploy-notes-worker-src/app.py /opt/deploy-notes-worker/app.py &&
sudo systemctl restart deploy-notes-worker.service

Repeat the output check. You don’t need to recreate the account, virtual environment or service for a code-only update. Before using the relative filenames in earlier commands, run cd ~/deploy-notes-worker-src. If packages changed, install the reviewed requirements into the same environment before restarting. Changes to the service file need its install/verify/daemon-reload commands from step 5, followed by sudo systemctl restart deploy-notes-worker.service.

Fix common problems

  • SSH Permission denied or timeout: check the provider’s login/key, IP address and SSH access rules. This happens before Python setup.
  • sudo refuses access: use the VPS administrator login. Don’t run the application as root to work around file permissions.
  • Python can’t open app.py: check that nano saved that exact name in ~/deploy-notes-worker-src and that the install block succeeded.
  • IndentationError or SyntaxError: inspect the named line in your editable file, including spaces. Fix it, then copy it into place again.
  • ModuleNotFoundError: the server environment is missing a dependency. Use your reviewed requirements in step 3; laptop-installed packages don’t transfer automatically.
  • 203/EXEC or 200/CHDIR: check the full interpreter, script and working-directory paths against the service file above.
  • Permission denied or Read-only file system in the logs: check the path being used. Keep output in /var/lib/deploy-notes-worker; home folders and writing beside the installed code are blocked.
  • active, but no progress: inspect the script’s output. A hung network request may leave it running; crash retries help only when it exits with an error. A normal exit also stays stopped with Restart=on-failure.
  • Checker 403 or 429: the page may refuse automated requests or require a slower rate. Respect its access rules; this is not proof of an outage.

After fixing repeated crashes that caused start-limit-hit, clear the paused retry state:

sudo systemctl reset-failed deploy-notes-worker.service &&
sudo systemctl start deploy-notes-worker.service

What this setup cannot guarantee

The checker observes HTTP responses, not whether a visitor can sign in or complete a purchase. It can miss problems between checks. If it shares a VPS with the website, a server outage takes both offline. Use a separate machine if you need an outside view.

For your own program, use timeouts for network operations and decide how it avoids repeating completed work after a restart. Systemd starts a new process; it doesn’t resume an interrupted task halfway through.

To check actual boot recovery, reboot a disposable test VPS, reconnect with your same full SSH command (including -i and your chosen key file if you followed our VPS setup guide), and repeat step 5. This interrupts everything on that server and is separate from the SSH-disconnection check above.

← Back to servers & scripts guides