---
slug: "raspberry-pi-signage-playwright-chromium-kiosk"
title: "Turning a Raspberry Pi into digital signage — maximizing Chromium with Playwright automation"
description: "Turn a Raspberry Pi into a kiosk signage display with Playwright (Python) + Chromium `--kiosk` for always-on fullscreen web pages. Persist login via cookies, auto-start via `.desktop`, and deploy updates with `scp`."
url: "https://www.ytyng.com/en/blog/raspberry-pi-signage-playwright-chromium-kiosk"
publish_date: "2026-02-18T03:00:00Z"
created: "2026-02-18T00:17:14.650Z"
updated: "2026-02-27T10:29:36.411Z"
categories: []
keywords: ""
featured_image_url: "https://media.ytyng.com/resize/20260226/139fba997e1e472497fa00d7b1927c84.png.webp?width=768"
has_video: true
has_music: true
video_urls: ["https://media.ytyng.net/ytyng-blog/335/featured-video-1.mp4", "https://media.ytyng.net/ytyng-blog/335/featured-video-2.mp4", "https://media.ytyng.net/ytyng-blog/335/featured-video-3.mp4"]
music_urls: ["https://media.ytyng.net/ytyng-blog/335/featured-music-335-2.mp3", "https://media.ytyng.net/ytyng-blog/335/featured-music-335-3.mp3"]
lang: "en"
---

# Turning a Raspberry Pi into digital signage — maximizing Chromium with Playwright automation

## Overview

A setup was built to use a Raspberry Pi as a “signage terminal” that continuously displays an internal web application in full-screen mode.

For browser automation, **Playwright** (Python) is used, and full-screen display is achieved via Chromium’s `--kiosk` mode.

### What this setup can do

- When the Raspberry Pi boots, Chromium automatically launches and displays the specified web page in full-screen mode
- Automatically authenticates even for pages that require login
- Persists cookies to keep the logged-in state even after reboot
- During development, can switch to windowed mode with the `--no-fullscreen` option

## Environment

- Raspberry Pi 5 / Raspberry Pi OS (Debian bookworm)
- Python 3.13
- [uv](https://docs.astral.sh/uv/) (Python package manager)
- [Playwright](https://playwright.dev/python/)

## Setup

### Install uv

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

### Project dependencies

`pyproject.toml`:

```toml
[project]
name = "rpi-signage"
version = "0.1.0"
description = "Automated Web page launcher for Raspberry Pi using Playwright"
requires-python = "==3.13.*"
dependencies = [
    "playwright>=1.57.0",
    "python-dotenv>=1.2.1",
]
```

### Install Playwright and Chromium

```bash
uv sync
uv run playwright install chromium
```

With `playwright install chromium`, the Chromium binary required for Playwright to automate the browser is downloaded. This is different from the Chromium preinstalled on Raspberry Pi OS.

However, if Chromium is already installed on the OS, that one is preferred (see `get_browser_channel()` below).

## Python code

### Main script

```python
#!/usr/bin/env python3

import argparse
import json
import shutil
import time
from pathlib import Path

import dotenv
from playwright._impl._errors import TargetClosedError
from playwright.sync_api import BrowserContext, sync_playwright

COOKIES_FILE = Path(__file__).parent / "cookies.json"


def save_cookies(context: BrowserContext):
    """Cookie を JSON ファイルに保存する"""
    try:
        cookies = context.cookies()
        COOKIES_FILE.write_text(json.dumps(cookies, ensure_ascii=False))
        print(f"Saved {len(cookies)} cookies.")
    except Exception as e:
        print(f"Failed to save cookies: {e}")


def load_cookies(context: BrowserContext):
    """保存済みの Cookie を読み込んでブラウザコンテキストに適用する"""
    if not COOKIES_FILE.exists():
        return
    try:
        cookies = json.loads(COOKIES_FILE.read_text())
        context.add_cookies(cookies)
        print(f"Loaded {len(cookies)} cookies.")
    except Exception as e:
        print(f"Failed to load cookies: {e}")


def get_browser_channel() -> str | None:
    """OS にインストールされた Chromium/Chrome を検出する。
    なければ None を返し、Playwright バンドル版を使用する。"""
    if shutil.which("chromium") or shutil.which("chromium-browser"):
        return "chromium"
    if shutil.which("google-chrome") or shutil.which("chrome"):
        return "chrome"
    return None


def main(args):
    with sync_playwright() as p:
        channel = get_browser_channel()
        chromium_args = [
            "--window-size=1920,1040",
            "--window-position=0,0",
        ]
        if not args.no_fullscreen:
            chromium_args.append("--kiosk")

        context = p.chromium.launch_persistent_context(
            user_data_dir="./browser_data",
            headless=False,
            channel=channel,
            args=chromium_args,
            no_viewport=True,
            ignore_https_errors=True,
        )
        page = context.pages[0] if context.pages else context.new_page()
        load_cookies(context)

        page.goto("https://your-webapp.example.com/")
        time.sleep(2)

        # ログインページにリダイレクトされた場合の認証処理
        if page.locator("#username").count() > 0:
            print("Logging in...")
            page.fill("#username", dotenv.get_key(".env", "USERNAME"))
            page.fill("#password", dotenv.get_key(".env", "PASSWORD"))
            page.click("button[type='submit']")
            time.sleep(3)
            save_cookies(context)

        # Ctrl+C で終了するまで無限待機
        print("Press Ctrl+C to exit.")
        try:
            while True:
                time.sleep(2)
        except KeyboardInterrupt:
            save_cookies(context)
        except TargetClosedError:
            print("Browser window was closed.")

        context.close()


if __name__ == "__main__":
    arg_parser = argparse.ArgumentParser()
    arg_parser.add_argument(
        "--no-fullscreen",
        action="store_true",
        help="フルスクリーンにしない。開発時に指定する。",
    )
    args = arg_parser.parse_args()
    main(args)
```

### Key points in the code

#### `launch_persistent_context`

Instead of Playwright’s standard `launch()`, `launch_persistent_context()` is used.
This persists the browser profile data (cache, local storage, etc.) under `user_data_dir`.

#### Cookie persistence

`save_cookies()` / `load_cookies()` save and restore cookies to/from a JSON file.
While `launch_persistent_context` preserves some session state, explicitly saving cookies makes it more reliable to restore the logged-in state.

#### `get_browser_channel()`

On Raspberry Pi OS, `chromium-browser` may be preinstalled.
Using `shutil.which()`, it detects Chromium/Chrome installed on the system and uses it if available; otherwise it falls back to Playwright’s bundled Chromium.

Native OS Chromium tends to perform better on a Raspberry Pi.

#### `no_viewport=True`

By default, Playwright fixes the viewport size. Setting `no_viewport=True` makes the viewport adjust dynamically to the window size. This is necessary for signage use where the entire display should be utilized.

#### Loading credentials from `.env`

Credentials such as passwords are stored in a `.env` file and loaded via `python-dotenv`.

```
USERNAME=your-username
PASSWORD=your-password
```

## Trial and error for full-screen mode

Several approaches were tried to display Chromium in full-screen on the Raspberry Pi.

### Approaches that didn’t work

#### 1. `xdotool key F11`

A method that sends the F11 key with a delayed execution from a shell script.

```bash
sleep 10 && xdotool key F11 &
```

It wasn’t stable due to timing issues—sometimes the key was sent before the browser fully started, or focus wasn’t on the right window.

#### 2. `--start-maximized`

A Chromium launch option. It maximizes the window, but the address bar and tab bar remain, which is insufficient for signage.

#### 3. JavaScript `requestFullscreen()`

A method that runs JavaScript from Playwright to enter full-screen.

```python
page.evaluate("document.documentElement.requestFullscreen()")
```

Programmatic calls without a user gesture (such as a click) may be blocked by the browser’s security policy.

### Chosen approach: `--kiosk`

```python
chromium_args.append("--kiosk")
```

Chromium’s `--kiosk` flag hides all UI elements including the address bar and tab bar, displaying the page in true full-screen mode.

It’s designed for kiosk terminals (public terminals) and is ideal for signage as well.

During development, adding the `--no-fullscreen` option disables `--kiosk` so behavior can be checked in a normal window.

```bash
# Normal launch (kiosk mode)
uv run ./open_signage.py

# During development (window mode)
uv run ./open_signage.py --no-fullscreen
```

## Configure desktop autostart

### Shell script

Shell script to run on autostart:

```bash
#!/usr/bin/env bash
set -e
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting signage"
cd "$(dirname "$0")"
export DISPLAY=:0
/home/pi/.local/bin/uv run ./open_signage.py
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Signage finished"
```

`export DISPLAY=:0` is required to access the X Window System from an autostart environment that otherwise lacks GUI context.

### `.desktop` file

By placing a `.desktop` file under `~/.config/autostart/`, the script runs automatically when the Raspberry Pi desktop starts.

```ini
[Desktop Entry]
Type=Application
Name=Web Signage
Exec=/bin/bash -c "/home/pi/signage/autostart.sh >> /tmp/signage.log 2>&1"
Terminal=false
X-GNOME-Autostart-enabled=true
```

Since logs are redirected to `/tmp/signage.log`, check this file if any issues occur.

## Deploying to the Raspberry Pi from a Mac

A simple deploy script that transfers files via `scp`:

```bash
#!/usr/bin/env zsh
set -e
cd "$(dirname "$0")"

FILES=(
  autostart.sh
  open_signage.py
  .env
  pyproject.toml
)

for FILE in "${FILES[@]}"; do
  scp "$FILE" pi@rpi-signage.local:~/signage/
done

scp signage.desktop pi@rpi-signage.local:~/.config/autostart/signage.desktop
```

After deployment, rebooting the Raspberry Pi will autostart the updated script.

On the Raspberry Pi side, run the following once for initial setup:

```bash
mkdir -p ~/signage
mkdir -p ~/.config/autostart
cd ~/signage
uv sync
uv run playwright install chromium
```

## Summary

- Built a signage terminal by automating Chromium with Playwright + Python
- The `--kiosk` flag is the most reliable way to force Chromium into full-screen
- Cookie persistence enables automatic login even for pages that require authentication
- Autostart at OS boot is configured via a `.desktop` file
- Dependencies are managed simply with `uv`
- A `scp`-based deploy script makes it easy to deploy from a Mac to the Raspberry Pi
