How to Schedule Website Screenshots on a Linux Server with systemd Timers
Schedule unattended website screenshots with a systemd timer and Playwright. Configure catch-up behavior, troubleshoot failures, and save captures reliably.
Use a systemd .timer unit to activate a matching .service unit, and have that service run a script that captures the page. In this guide, Playwright runs headlessly, saves a full-page PNG to an absolute path, and exits. OnCalendar= schedules a wall-clock time; Persistent=true can trigger one catch-up run after the timer was inactive during a scheduled event.
1. Install Playwright and its browser
Run the capture as a dedicated Linux account. Install Node.js and Playwright for that account or in the project directory, then install the matching Chromium browser and Linux dependencies. Playwright browser binaries need to match the installed Playwright version.
sudo useradd --system --create-home --home-dir /opt/site-screenshot screenshot
sudo mkdir -p /opt/site-screenshot /var/lib/site-screenshot
sudo chown -R screenshot:screenshot /opt/site-screenshot /var/lib/site-screenshot
cd /opt/site-screenshot
sudo -u screenshot npm init -y
sudo -u screenshot npm install playwright
sudo -u screenshot npx playwright install --with-deps chromium
Package names and privilege requirements vary by distribution. Follow the Playwright browser installation guide if your system requires a different setup. Playwright runs headless by default; headed Linux runs require a display server such as Xvfb.
2. Create the capture script
Save this as /opt/site-screenshot/capture.js. It sets explicit navigation and screenshot timeouts, writes to a stable absolute path, and closes the browser even if navigation or capture fails.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1,
});
await page.goto('https://example.com', {
waitUntil: 'load',
timeout: 60000,
});
await page.screenshot({
path: '/var/lib/site-screenshot/latest.png',
fullPage: true,
timeout: 30000,
});
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Replace the example URL and output path with your own. The service account must be able to write to the destination directory. Playwright resolves relative screenshot paths against the current working directory, so use an absolute path or set WorkingDirectory= in the service. The script uses both: its output is absolute, and the unit below sets a working directory.
3. Define the systemd service and timer
Create /etc/systemd/system/site-screenshot.service:
[Unit]
Description=Capture a website screenshot
[Service]
Type=oneshot
User=screenshot
WorkingDirectory=/opt/site-screenshot
ExecStart=/usr/bin/node /opt/site-screenshot/capture.js
Check the Node executable path with command -v node and update ExecStart= if Node.js is installed elsewhere. The matching unit name lets site-screenshot.timer activate site-screenshot.service by default.
Create /etc/systemd/system/site-screenshot.timer:
[Unit]
Description=Schedule website screenshots
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
This example schedules a daily run at 02:00 according to the server’s local time. Confirm how systemd parses a schedule on the target host before enabling it:
systemd-analyze calendar '*-*-* 02:00:00'
4. Enable, run, and inspect the job
Reload unit files, enable the timer at boot, and start it now. You can also start the service directly for an immediate capture.
sudo systemctl daemon-reload
sudo systemctl enable --now site-screenshot.timer
sudo systemctl start site-screenshot.service
systemctl list-timers site-screenshot.timer
sudo systemctl status site-screenshot.timer site-screenshot.service
sudo journalctl -u site-screenshot.service
Check that /var/lib/site-screenshot/latest.png exists and is readable by the intended consumer. A oneshot service becomes inactive after a successful run; that is normal. The timer remains active and schedules the next activation.
5. Choose the right schedule and catch-up behavior
| Need | Configuration | Behavior |
|---|---|---|
| Run at a wall-clock time | OnCalendar=*-*-* 02:00:00 |
Runs daily at the configured local time. |
| Run at a fixed interval | OnUnitActiveSec=1h |
Schedules relative to the service’s last activation, rather than at a particular clock time. |
| Catch up after timer downtime | Persistent=true with OnCalendar= |
When the timer starts again, systemd can trigger one activation if a calendar event elapsed while the timer was inactive. It does not replay every missed interval. |
| Skip missed events | Omit Persistent=true |
No catch-up activation for calendar events missed while the timer was inactive. |
Use a calendar schedule for reports or captures tied to a particular time of day. Use a monotonic interval for recurring work where elapsed time matters more than the clock. A timer’s activation is not an exact-second guarantee: AccuracySec= sets a window in which systemd may schedule the event. Current systemd documentation also describes ordering calendar timers after time-setting and synchronization targets so the clock is established; details can depend on the installed version and host setup.
For more complex calendars, consult systemd.timer and verify the expression with systemd-analyze calendar. For a single daily job, leave AccuracySec= at its default unless the capture has a real timing requirement.
6. Configure the screenshot for your use case
- Viewport or full page: omit
fullPageor set it tofalsefor the visible viewport; set it totrueto capture the full page. Very long pages can produce large images and take longer to render. - Stable dimensions: set
viewportanddeviceScaleFactorexplicitly when comparing captures over time. A larger scale factor creates a higher-resolution image and uses more memory and storage. - Wait for page readiness:
waitUntil: 'load'waits for the load event. If the page fills in later, wait for a specific selector or add a bounded delay before taking the screenshot. Avoid waiting indefinitely for network idle on pages with continuous background requests. - Lazy-loaded content: full-page capture does not guarantee every site’s lazy content has loaded. If necessary, scroll through the page in the script, wait for images or key selectors, then capture. Test the page’s own behavior because lazy loading differs across sites.
- Format and output: Playwright infers PNG, JPEG, or WebP from the filename extension. Use
.jpgwith aqualityvalue for JPEG; quality is applicable to JPEG and WebP. Choose the format based on downstream use and file size.
Playwright’s Page screenshot API documents screenshot options and the rule for relative paths. Browser version, host OS, fonts, settings, hardware, power source, and headless mode can all affect rendering. Keep the server image, browser build, fonts, viewport, and capture settings consistent if you compare screenshots over time.
7. Troubleshoot missed runs and bad captures
| Symptom | Likely cause | Fix |
|---|---|---|
Timer is absent from list-timers |
Unit files were not reloaded, or the timer is not enabled. | Run sudo systemctl daemon-reload, then sudo systemctl enable --now site-screenshot.timer. Check spelling and unit paths. |
| Timer fires at an unexpected time | Calendar expression or local time zone differs from your assumption; activation may also fall within the AccuracySec= window. |
Run systemd-analyze calendar for the expression and check the server’s clock and time zone. |
| Service fails with “No such file or directory” | The Node path, script path, or working directory is wrong. | Check command -v node, confirm the script exists, and correct ExecStart= or WorkingDirectory=. |
| Permission denied while saving | The service account cannot write to the output directory. | Create the directory and assign access to the configured User=; verify the full output path. |
| Browser executable is missing | Chromium was not installed for the installed Playwright version, or it was installed as another user. | As the service account, run npx playwright install chromium. If Linux libraries are missing, install the required dependencies using the official browser guide. |
| Navigation times out | The site is slow, unreachable from the server, or keeps requests open. | Check network and DNS from the host, use an appropriate finite navigation timeout, and choose a readiness condition that matches the page. |
| Capture is blank or missing content | The screenshot ran before the relevant content rendered, or the page requires interaction. | Wait for a selector that identifies the content, perform required interactions, or add a bounded delay. Inspect service logs for script errors. |
| Output is unexpectedly in another directory | A relative screenshot path was resolved from a service working directory different from the interactive shell. | Use an absolute path or explicitly set WorkingDirectory=. |
| Screenshots differ between runs | Browser, operating system, fonts, viewport, or other rendering inputs changed. | Pin and maintain the environment and capture settings; review Playwright’s screenshot comparison guidance. |
Use sudo journalctl -u site-screenshot.service --since today to inspect recent failures. After editing a unit file, run sudo systemctl daemon-reload. After changing the timer, restart it with sudo systemctl restart site-screenshot.timer so the new schedule is loaded.
8. Performance, reliability, and cost
A local Playwright job consumes the server’s CPU, memory, and disk while Chromium renders the page. Full-page captures, high device scale factors, and heavy pages can increase those demands. Schedule large jobs away from other peak workloads, keep output retention bounded, and avoid launching overlapping captures if one run may last longer than the interval. A oneshot service does not by itself provide a queue for every overlapping scheduled occurrence.
For reliability, keep browser installation and Playwright versions aligned, use explicit paths, return a nonzero exit status on failure, and retain logs long enough to diagnose problems. Persistent=true covers a missed calendar activation when the timer was inactive; it is not a retry policy for a service that ran and failed. Add retries only when appropriate for the target site and make them bounded to avoid excessive requests. Rendering consistency requires a stable host and browser environment; systemd scheduling alone cannot make page output deterministic.
There is no per-capture software fee established by this implementation pattern, but the server still has infrastructure, storage, and maintenance costs. Browser dependencies and saved files require disk space, and frequent captures consume compute. Set a retention or rotation policy if each run should be kept instead of overwriting latest.png.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Call it from a scheduled script or any HTTP client; its API documentation covers the request options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Cookie banners and consent overlays, newsletter popups, and chat widgets are removed before the capture. Bot checks, blank pages, and failed loads are never billed; response headers identify the page verdict and billing status. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
FAQ
Does Persistent=true rerun every screenshot missed while the server was off?
No. When the timer becomes active again, it can trigger one catch-up activation if a calendar event elapsed during timer inactivity.
Will systemd run the screenshot at the exact second in OnCalendar?
Not necessarily. Timer accuracy settings allow a scheduling window, and clock synchronization and systemd version affect calendar timer behavior.
Do I need a graphical desktop on the server?
No for this headless Playwright example. Playwright runs headless by default; headed Linux browser operation requires a display server such as Xvfb.
Why are captures not pixel-identical after a server update?
Browser version, operating system, fonts, hardware, and capture settings can change page rendering. Keep those inputs consistent when visual comparisons matter.


