Make the app runnable in Docker for GUI and CLI

Build one shared container image for the Flask GUI and CLI pipeline, with Playwright, FFmpeg, and spaCy preinstalled so first runs are reliable. Add bootstrap logic for missing runtime files, bind the GUI to 0.0.0.0 in containers, and preserve state through a repo mount.

Constraint: Local development needs a single image that supports both entrypoints without introducing extra services or dependencies.
Rejected: Separate GUI and CLI images | duplicated maintenance and no runtime benefit for this repo.
Confidence: high
Scope-risk: moderate
Directive: Keep runtime state creation in the container bootstrap layer; do not reintroduce host-specific assumptions into GUI startup.
Tested: docker compose build; docker compose run --rm gui python -c '...'; docker compose run --rm cli python -c 'import main'; docker compose up -d gui; curl -I http://localhost:4000
Not-tested: Full end-to-end video generation with live credentials in this environment.
pull/2551/head
Hong Phuc 1 month ago
parent c2f394f549
commit 4d8c393a94

@ -1,2 +1,20 @@
Dockerfile
results
.git
.github
.omx
.venv
venv
__pycache__
*.pyc
*.pyo
*.pyd
.pytest_cache
.mypy_cache
.ruff_cache
.DS_Store
config.toml
results
assets/temp
assets/backgrounds
video_creation/data/videos.json
video_creation/data/cookie-threads.json
out

4
.gitignore vendored

@ -242,7 +242,11 @@ reddit-bot-351418-5560ebc49cac.json
/.idea
*.pyc
video_creation/data/videos.json
video_creation/data/cookie-threads.json
video_creation/data/envvars.txt
utils/backgrounds.json
config.toml
*.exe
.omx

@ -1,12 +1,27 @@
FROM python:3.10.14-slim
FROM python:3.10-slim-bookworm
RUN apt update
RUN apt-get install -y ffmpeg
RUN apt install python3-pip -y
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN mkdir /app
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python3", "main.py"]
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg \
curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt ./
RUN pip install --upgrade pip \
&& pip install -r requirements.txt \
&& python -m spacy download en_core_web_sm
RUN python -m playwright install --with-deps chromium
COPY . .
RUN chmod +x /app/docker-entrypoint.sh
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]

@ -1,8 +1,9 @@
import webbrowser
from pathlib import Path
import os
import webbrowser
from pathlib import Path
# Used "tomlkit" instead of "toml" because it doesn't change formatting on "dump"
import tomlkit
import tomlkit
from flask import (
Flask,
redirect,
@ -12,12 +13,16 @@ from flask import (
url_for,
)
import utils.gui_utils as gui
# Set the hostname
HOST = "localhost"
# Set the port number
PORT = 4000
import utils.gui_utils as gui
from utils.docker_bootstrap import ensure_runtime_state
ensure_runtime_state()
# Set the hostname and port
HOST = os.environ.get("GUI_HOST", "0.0.0.0")
PORT = int(os.environ.get("GUI_PORT", "4000"))
OPEN_BROWSER = os.environ.get("GUI_OPEN_BROWSER", "1").lower() in {"1", "true", "yes", "on"}
BROWSER_URL = os.environ.get("GUI_BROWSER_URL", f"http://localhost:{PORT}")
# Configure application
app = Flask(__name__, template_folder="GUI")
@ -110,7 +115,8 @@ def voices(name):
# Run browser and start the app
if __name__ == "__main__":
webbrowser.open(f"http://{HOST}:{PORT}", new=2)
print("Website opened in new tab. Refresh if it didn't load.")
app.run(port=PORT)
if __name__ == "__main__":
if OPEN_BROWSER:
webbrowser.open(BROWSER_URL, new=2)
print("Website opened in new tab. Refresh if it didn't load.")
app.run(host=HOST, port=PORT)

@ -34,6 +34,7 @@ The only original thing being done is the editing and gathering of all materials
- Python 3.10
- Playwright (this should install automatically in installation)
- Docker and Docker Compose for the container workflow
## Installation 👩‍💻
@ -66,6 +67,44 @@ The only original thing being done is the editing and gathering of all materials
python -m playwright install-deps
```
## Docker
The repository now includes a shared image plus Compose services for the GUI and CLI.
Build the image:
```sh
docker compose build
```
Start the GUI:
```sh
docker compose up gui
```
Open `http://localhost:4000` in your browser.
Run the CLI pipeline:
```sh
docker compose run --rm cli
```
Run the CLI for a specific post:
```sh
docker compose run --rm cli python main.py <post_id>
```
Stop the GUI and remove the Compose stack:
```sh
docker compose down
```
The repo root is bind-mounted into the container so `config.toml`, `results/`, `assets/temp/`, and the runtime JSON files persist across rebuilds and repeated runs.
---
**EXPERIMENTAL!!!!**

@ -1,2 +1,3 @@
#!/bin/sh
docker build -t rvmt .
set -eu
docker compose build

@ -0,0 +1,27 @@
services:
gui:
build:
context: .
image: videomakerbot:latest
command: ["python", "GUI.py"]
ports:
- "4000:4000"
environment:
GUI_HOST: "0.0.0.0"
GUI_PORT: "4000"
GUI_OPEN_BROWSER: "0"
GUI_BROWSER_URL: "http://localhost:4000"
volumes:
- ./:/app
shm_size: "1gb"
cli:
build:
context: .
image: videomakerbot:latest
command: ["python", "main.py"]
environment:
PYTHONUNBUFFERED: "1"
volumes:
- ./:/app
shm_size: "1gb"

@ -0,0 +1,6 @@
#!/bin/sh
set -eu
python -m utils.docker_bootstrap
exec "$@"

@ -1,2 +1,3 @@
#!/bin/sh
docker run -v $(pwd)/out/:/app/assets -v $(pwd)/.env:/app/.env -it rvmt
set -eu
docker compose run --rm cli "$@"

@ -13,7 +13,7 @@ def cleanup(reddit_id) -> int:
Returns:
int: How many files were deleted
"""
directory = f"../assets/temp/{reddit_id}/"
directory = f"assets/temp/{reddit_id}/"
if exists(directory):
shutil.rmtree(directory)

@ -0,0 +1,70 @@
"""Container bootstrap helpers for first-run runtime state."""
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict
import tomlkit
ROOT = Path(__file__).resolve().parent.parent
def _default_from_template(node: Dict[str, Any]) -> Dict[str, Any]:
defaults: Dict[str, Any] = {}
for key, value in node.items():
if isinstance(value, dict) and "optional" in value:
if "default" in value:
defaults[key] = value["default"]
else:
value_type = value.get("type")
if value_type == "bool":
defaults[key] = False
elif value_type in {"int", "float"}:
defaults[key] = 0
else:
defaults[key] = ""
elif isinstance(value, dict):
defaults[key] = _default_from_template(value)
return defaults
def _ensure_json(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
path.write_text(content, encoding="utf-8")
def _ensure_config(path: Path) -> None:
if path.exists():
return
template_path = ROOT / "utils/.config.template.toml"
template = tomlkit.loads(template_path.read_text(encoding="utf-8"))
defaults = _default_from_template(template)
path.write_text(tomlkit.dumps(defaults), encoding="utf-8")
def ensure_runtime_state() -> None:
"""Create runtime files and directories expected by the app."""
for relative in (
"assets/temp",
"assets/backgrounds/audio",
"assets/backgrounds/video",
"results",
"video_creation/data",
):
(ROOT / relative).mkdir(parents=True, exist_ok=True)
_ensure_config(ROOT / "config.toml")
_ensure_json(ROOT / "video_creation/data/videos.json", "[]\n")
_ensure_json(ROOT / "utils/backgrounds.json", "{}\n")
def main() -> None:
ensure_runtime_state()
if __name__ == "__main__":
main()

@ -10,8 +10,8 @@ if TYPE_CHECKING:
def check_done(
redditobj: Submission,
) -> Submission:
redditobj: "Submission",
) -> "Submission":
# don't set this to be run anyplace that isn't subreddit.py bc of inspect stack
"""Checks if the chosen post has already been generated

Loading…
Cancel
Save