Moonraker Timelapse Docker Template
This service watches a Klipper/Moonraker printer, captures a JPEG whenever the reported toolhead Z position increases by a configured layer height, and renders the captured frames to an H.264 MP4 after a successful print. Snapshot directories and videos are named with the print filename and UTC start date/time, for example benchy_20260905-210500 and benchy_20260905-210500.mp4.
Quick start
-
Copy
.env.exampleto.env. -
Set
MOONRAKER_URLandCAMERA_SNAPSHOT_URLto the endpoints used by your printer and Mainsail. -
Start the service:
docker compose up --build -d -
Open the panel at
http://localhost:8080. -
Find rendered videos under
./timelapses.
Storage is organized with videos in the root timelapse folder and snapshots in a matching print folder:
timelapses/
├── benchy_20260905-210500.mp4
└── benchy_20260905-210500/
├── frame-000000.jpg
└── frame-000001.jpg
The panel is also available when running locally with ./run.sh at http://localhost:8080. Set WEB_PORT to use another port. It shows Moonraker connectivity, printer state, current Z height, the latest captured snapshot, and downloadable completed videos. The JSON endpoints are /api/status and /api/videos; the latest JPEG is available at /api/snapshot.
To build the image directly instead of using Compose:
./build.sh
The default image is moonraker-timelapse:latest. Override it when publishing or testing a specific version:
IMAGE_NAME=registry.example.com/you/timelapse IMAGE_TAG=0.1.0 ./build.sh
For local development without Docker, install Python 3 and ffmpeg, then run:
./run.sh
The script loads .env when present, defaults OUTPUT_DIR to ./timelapses, and uses 127.0.0.1:7125 as the local Moonraker default. Set MOONRAKER_URL and CAMERA_SNAPSHOT_URL in .env when the printer or camera is on another host.
The default camera URL is compatible with common MJPEG camera configurations using ?action=snapshot. For a camera that exposes a different still-image endpoint, set CAMERA_SNAPSHOT_URL accordingly.
Render settings
Render behavior can be configured in .env:
| Variable | Default | Purpose |
|---|---|---|
VIDEO_FPS |
12 |
Playback frame rate. |
VIDEO_DUPLICATE_END_FRAMES |
0 |
Copies of the final snapshot appended to hold the end of the video. |
VIDEO_CODEC |
libx264 |
FFmpeg video encoder. |
VIDEO_PRESET |
medium |
Encoder speed/compression preset. |
VIDEO_CRF |
18 |
H.264 quality; lower values produce larger, higher-quality files. |
VIDEO_PIXEL_FORMAT |
yuv420p |
Output pixel format for broad player compatibility. |
For example, VIDEO_FPS=24 and VIDEO_DUPLICATE_END_FRAMES=48 creates a two-second hold on the final snapshot.
How layer detection works
The worker polls Moonraker's print_stats and gcode_move objects. When slicer layer metadata (print_stats.info.current_layer) is available, it captures once per reported layer. Otherwise it falls back to detecting Z increases using MIN_LAYER_HEIGHT_MM; the default poll interval is 0.5 seconds, but layer metadata is recommended because a slow poll can miss a short Z move.
Development smoke test
The Python worker uses only the standard library. Validate syntax locally with:
python3 -m py_compile app.py
docker compose config
The service intentionally treats Moonraker and camera failures as recoverable polling errors, so a printer or camera reboot does not stop the container.