Skip to contentLaunch price30% off every kit for the first 250 buyers155 left
DirectoryLab

Loading the guides…

Launch LabGuides
Go live

Backups and scheduled jobs

Know what runs in the background and check that it is running, then back up and restore a site on a server.

Updated 9 Oct 2026 · written for Launch Lab 2.9.0

On this page

Open System under Tools in the admin sidebar. Its Background jobs table lists the seven jobs that put booked listings on the site, send queued email and keep adverts in step with Stripe, with when each last ran. This guide covers those jobs, then the nightly backup on a server set up with deploy.sh and how to restore one. Server commands are run as root in the kit's folder, here /opt/launchlab, for a site called gadgets.

The background jobs on the System page: each job with what it does, how often it runs, when it last ran, its result and a Run now button
Admin, System

The guides say "listing" and "maker". Your site uses its own words for both.

The seven jobs#

JobRunsWhat it does
Scheduled launchesOnce a dayPuts every booked listing whose date has arrived on the site, including any from days the job missed. On launch day it emails last week's makers how their week went. It frees places held for 24 hours by a checkout nobody paid and sends the once-only reminders: a draft left alone, a request to review the site a week after launch, a featured place about to end.
Featured expiryHourlyEnds featured listings that have reached their end date, and emails the people waiting for a featured place.
Email automationsHourlySends your automations to the people who have newly qualified for one.
Queued campaignsEvery 10 minutesSends the campaigns you queued in the admin.
Advert subscriptionsOnce a dayAsks Stripe about every advert subscription, in case a webhook was missed.
Daily statsHourlyRecords the figures behind the analytics charts.
Security cleanupOnce a dayDeletes security events more than 30 days old.
  • Once a day means once in each day by UTC, on the first pass at or after the launch hour. That hour is Hour (UTC, 0 to 23) under Weekly launch schedule on Payment Settings, and it starts at 9. A once-a-day job that fails is tried again on the next pass.

  • Hourly means when 55 minutes have gone by since the last run.

  • The jobs are looked at every 15 minutes, so Every 10 minutes means every time.

What runs the jobs#

One command, flask jobs run, works out which jobs are due and runs them. Something has to call it, and what does depends on how the site is installed.

InstallWhat calls it
A server set up with deploy.shA cron line, installed by first-run and again by every update. Every 15 minutes it runs scripts/deploy.sh jobs, which runs the due jobs inside each site's container in turn and writes what happened to sites/jobs.log.
Docker ComposeThe jobs container. It runs the due jobs, sleeps 15 minutes and starts again.
A server without DockerNothing, until you add a cron line yourself.
Your own computerNothing. Booked listings stay booked until you click Run now.

On a deploy.sh server, crontab -l shows the two lines the kit installed:

crontab
*/15 * * * * /opt/launchlab/scripts/deploy.sh jobs >> /opt/launchlab/sites/jobs.log 2>&1 # launchlab-jobs
30 3 * * * /opt/launchlab/scripts/backup-all.sh auto >> /opt/launchlab/backups/backup.log 2>&1 # launchlab-backup

scripts/setup-crons.sh writes them. Running it again is safe: it replaces its own two lines and leaves every other line alone. The lines carry the folder's path, so run it again if you ever move the folder.

Without Docker, add the line the System page shows above the table, with crontab -e:

text
*/15 * * * * cd /opt/launchlab && .venv/bin/flask --app run.py jobs run

Check that the jobs are running#

  1. Step 1: Open System

    Open System under Tools and find Background jobs. Each row has the job, how often it Runs, its Last run (UTC) and its Result.

  2. Step 2: Read the last run

    "Never" in every row means nothing is calling the job runner. On a working server the hourly jobs show a time in the last hour. Result reads "OK" or "Failed". Hold the pointer over "Failed" to read why.

  3. Step 3: Click Run now

    Run now runs that one job at once, due or not. The page comes back with "scheduled_launches finished." or with "scheduled_launches failed:" and the reason.

The Finish setting up list on the admin dashboard has an item called "Schedule the job runner". It is ticked while any job has run in the last two hours. Run now counts, so on your own computer it ticks for two hours and then clears again.

The admin dashboard of a site that has just been set up: a Finish setting up list with the site address ticked and the rest still to do, among them connect email, connect Stripe, add the Stripe webhook secret, schedule the job runner, fill in the legal details and get a first launch live, each with a Set up link
A new site lists what is left to set up, with a link to each.

Run a job from the terminal#

Under deploy.sh
docker exec launchlab-gadgets flask jobs status
docker exec launchlab-gadgets flask jobs run --only scheduled_launches

Under Compose, start each line with docker compose exec app flask. Without Docker, start it with venv/bin/flask --app run.py.

  • jobs status prints a line for each job: its name, then how and when it last ran, such as "ok at 2026-10-08 09:00 UTC", or "never run".

  • jobs run runs what is due and prints "ok" or "FAILED" beside each name, or "Nothing due."

  • --only NAME runs that one job now, due or not. The names are scheduled_launches, featured_expiry, email_automations, queued_campaigns, advert_subscriptions, daily_stats and security_cleanup.

  • --force runs all seven now. With --only it changes nothing.

The nightly backup#

At 03:30 by the server's clock, cron runs scripts/backup-all.sh auto. It covers the sites deploy.sh made and nothing else. For each one it writes a single archive, named with the date and time in UTC:

text
/opt/launchlab/backups/daily/gadgets-20261007-033001.tar.gz

The archive holds:

  • The database, instance/app.db, copied while the site is running in a way that is safe to restore.

  • The uploads, the whole of instance/uploads, every time.

  • The settings: the site's .env, .port and .domain files. Also instance/secret_key, if the site has that file.

Which folder it goes in depends on the date, in UTC, and each folder keeps its newest archives for each site:

FolderWhenKept
monthlyThe 1st of the month6
weeklyA Sunday that is not the 1st4
dailyEvery other day7
  • Disk check: if the disk is 80% full or more, the script writes nothing and logs "ERROR: disk is 83% full; not writing more backups".

  • Size check: an archive under 1 KB is deleted and counted as a failure.

  • The log is backups/backup.log. A good run ends "Backup complete: 1 site(s), daily."

To run one yourself, for example before an update:

bash
scripts/deploy.sh backup
scripts/deploy.sh backup weekly

The first writes to daily. The second names the folder.

What is not in a backup#

A site stopped with remove-site is no longer backed up. The snapshots that update keeps in instance/.snapshots are not in the archive either.

Get a Telegram message when a backup fails#

The script can send a Telegram message when a backup fails. It sends nothing when one works.

  1. Step 1: Make a bot

    In Telegram, open a chat with @BotFather and send /newbot. It asks for a name and replies with a token that looks like 123456789:AAH followed by a long string.

  2. Step 2: Find your chat ID

    Send your new bot any message. Then open https://api.telegram.org/bot<token>/getUpdates in a browser, with your token in place of <token>. Your chat ID is the number after "chat":{"id":.

  3. Step 3: Write the file

    /opt/launchlab/.backup.env
    TELEGRAM_BOT_TOKEN=your-bot-token
    TELEGRAM_CHAT_ID=your-chat-id
  4. Step 4: Test it

    bash
    scripts/backup-all.sh test

    There is no folder called test, so the script stops before it writes anything and sends "launchlab backup failed on" with the server's name and the reason.

Restore a backup#

There is no restore command. The route the kit supports is to unpack an archive and hand its instance folder to import-site, which replaces a site's data with a folder holding app.db, a file called secret_key and, if there is one, uploads. It works for SQLite sites only, and anything added since the backup was made is gone from the live site.

  1. Step 1: Unpack the archive

    bash
    mkdir -p /root/restore
    tar -xzf backups/daily/gadgets-20261007-033001.tar.gz -C /root/restore

    You now have /root/restore/gadgets, with .env and instance inside it.

  2. Step 2: Write the secret_key file

    bash
    grep '^SECRET_KEY=' /root/restore/gadgets/.env | cut -d= -f2- > /root/restore/gadgets/instance/secret_key
  3. Step 3: On a new server, make the site first

    Run first-run and add-site gadgets example.com as in Deploy to a server. Do not open the setup link: your admin account arrives with the data.

  4. Step 4: Import

    bash
    scripts/deploy.sh import-site gadgets /root/restore/gadgets/instance

    The script stops the site, moves its present data aside, copies in the database and the uploads, writes the key into the site's .env and starts the site. It ends "Imported into 'gadgets'. The previous data, if any, is in sites/gadgets/instance.before-import-" and the time.

  5. Step 5: Check the settings

    import-site takes only the key from the archive. Compare /root/restore/gadgets/.env with sites/gadgets/.env and bring across anything else you had set, with scripts/deploy.sh set-env. When you are sure of the restored site, delete the instance.before-import folder.

The same command moves a site from your own computer to the server: stop the site there, copy its instance folder up and import it. If your computer's .env has a SECRET_KEY line, write the secret_key file from it first, as above.

Backups without deploy.sh#

backup-all.sh looks only in the sites folder. Under Docker Compose, or without Docker, nothing is backed up for you. Everything a site owns is in its instance folder and its .env file. Stop the site, copy both and start it again:

Docker Compose
docker compose stop app jobs
tar -czf /root/launchlab-$(date +%F).tar.gz instance .env
docker compose start app jobs

The files in scripts/backup/ and scripts/restore_from_backup.py are older tools that copy the SQLite file, or call pg_dump and restore its .dump files, and the nightly backup neither uses them nor writes anything they can read.

How do I know last night's backup worked?
bash
tail /opt/launchlab/backups/backup.log
ls -lh /opt/launchlab/backups/daily/

The log ends with a line beginning "Backup complete", and the newest archive has last night's date and a sensible size.

Can I change the times?

You can edit the two lines with crontab -e, but setup-crons.sh writes its own two lines back whenever it runs, and both first-run and update run it.

The server was off on launch day. Are the listings lost?

No. The next time Scheduled launches runs, it puts every booking whose date has passed on the site, dated the day it runs.

Does a new site need its own cron line?

No. The one line runs the jobs for every site in the sites folder, and the backup covers them all.

Stuck on a step? Send a message.