fix: container volumes
Build and Publish Container / Publish Linux container (push) Successful in 4m4s

This commit is contained in:
2026-10-05 15:25:09 -04:00
parent 86b8ab9c75
commit b2df81130e
6 changed files with 77 additions and 4 deletions
+7 -3
View File
@@ -4,7 +4,7 @@ ARG DEBIAN_FRONTEND=noninteractive
RUN dpkg --add-architecture i386 \ RUN dpkg --add-architecture i386 \
&& apt-get update \ && apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
ca-certificates curl lib32gcc-s1 python3 python3-pip python3-venv \ ca-certificates curl gosu lib32gcc-s1 python3 python3-pip python3-venv \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& useradd --create-home --shell /bin/bash progression \ && useradd --create-home --shell /bin/bash progression \
&& mkdir -p /opt/steamcmd/steamapps /srv/progression/.data \ && mkdir -p /opt/steamcmd/steamapps /srv/progression/.data \
@@ -19,9 +19,12 @@ RUN python3 -m venv /srv/venv \
COPY --chown=progression:progression progression_backend ./progression_backend COPY --chown=progression:progression progression_backend ./progression_backend
COPY --chown=progression:progression art ./art COPY --chown=progression:progression art ./art
USER progression COPY --chmod=755 docker-entrypoint.sh /usr/local/bin/progression-entrypoint
ENV PROGRESSION_HOST=0.0.0.0 \ ENV HOME=/home/progression \
USER=progression \
XDG_DATA_HOME=/home/progression/.local/share \
PROGRESSION_HOST=0.0.0.0 \
PROGRESSION_PORT=8000 \ PROGRESSION_PORT=8000 \
PROGRESSION_DB_PATH=.data/progression.db \ PROGRESSION_DB_PATH=.data/progression.db \
STEAMCMD_PATH=/opt/steamcmd/steamcmd.sh \ STEAMCMD_PATH=/opt/steamcmd/steamcmd.sh \
@@ -35,4 +38,5 @@ EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -fsS http://127.0.0.1:8000/api/health || exit 1 CMD curl -fsS http://127.0.0.1:8000/api/health || exit 1
ENTRYPOINT ["/usr/local/bin/progression-entrypoint"]
CMD ["/srv/venv/bin/python", "-m", "progression_backend"] CMD ["/srv/venv/bin/python", "-m", "progression_backend"]
+2
View File
@@ -19,4 +19,6 @@ On a new deployment, the hourly refresh timer stays paused while the first full
Workshop download runs. Its hard limit defaults to 12 hours and can be changed Workshop download runs. Its hard limit defaults to 12 hours and can be changed
with `INITIAL_DOWNLOAD_TIMEOUT_SECONDS`. Follow the per-mod title, Workshop ID, with `INITIAL_DOWNLOAD_TIMEOUT_SECONDS`. Follow the per-mod title, Workshop ID,
result, and completion percentage in Dockhand or with `docker compose logs -f`. result, and completion percentage in Dockhand or with `docker compose logs -f`.
Failed batches also include SteamCMD's output tail so network, subscription, and
filesystem errors are visible directly in the container log.
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
set -eu
prepare_volume() {
path="$1"
marker="$path/.progression-volume-ready"
mkdir -p "$path"
if [ ! -e "$marker" ]; then
echo "Preparing persistent volume permissions: $path"
chown -R progression:progression "$path"
touch "$marker"
chown progression:progression "$marker"
fi
}
prepare_volume /srv/progression/.data
prepare_volume /opt/steamcmd/steamapps
exec gosu progression "$@"
+12
View File
@@ -293,6 +293,18 @@ class RefreshService:
result = self.runner.run_batch( result = self.runner.run_batch(
batch, timeout_seconds=remaining batch, timeout_seconds=remaining
) )
if result.status != "completed":
logger.warning(
"SteamCMD pass %d batch %d/%d finished with "
"status=%s exit_code=%s after %.1fs. Output:\n%s",
pass_number,
pass_batch_number,
len(batches),
result.status,
result.exit_code,
result.elapsed_seconds,
result.output_tail or "(no SteamCMD output)",
)
self.database.add_download_attempt( self.database.add_download_attempt(
{ {
"run_id": run_id, "run_id": run_id,
+15
View File
@@ -116,6 +116,20 @@ class SteamCmdRunner:
def available(self) -> bool: def available(self) -> bool:
return self.executable_path() is not None return self.executable_path() is not None
def ensure_workshop_writable(self) -> None:
workshop_dir = self.content_dir.parent.parent
try:
workshop_dir.mkdir(parents=True, exist_ok=True)
self.content_dir.mkdir(parents=True, exist_ok=True)
probe = workshop_dir / ".progression-write-test"
probe.write_text("ok", encoding="utf-8")
probe.unlink()
except OSError as error:
raise PermissionError(
f"SteamCMD Workshop directory is not writable: {workshop_dir}. "
"Check the container volume ownership."
) from error
def run_batch( def run_batch(
self, self,
workshop_ids: list[str], workshop_ids: list[str],
@@ -129,6 +143,7 @@ class SteamCmdRunner:
invalid = [item for item in workshop_ids if not item.isdigit()] invalid = [item for item in workshop_ids if not item.isdigit()]
if invalid: if invalid:
raise ValueError(f"Invalid Workshop ids: {', '.join(invalid)}") raise ValueError(f"Invalid Workshop ids: {', '.join(invalid)}")
self.ensure_workshop_writable()
command = [executable, "+login", "anonymous"] command = [executable, "+login", "anonymous"]
for workshop_id in workshop_ids: for workshop_id in workshop_ids:
+22 -1
View File
@@ -23,7 +23,12 @@ from progression_backend.modlist import (
) )
from progression_backend.refresh import RefreshCoordinator, RefreshService from progression_backend.refresh import RefreshCoordinator, RefreshService
from progression_backend.steam_api import WorkshopDetails from progression_backend.steam_api import WorkshopDetails
from progression_backend.workshop import SteamCmdBatchResult, parse_about_xml, utc_now from progression_backend.workshop import (
SteamCmdBatchResult,
SteamCmdRunner,
parse_about_xml,
utc_now,
)
class FakeSteam: class FakeSteam:
@@ -176,6 +181,22 @@ class BackendTests(unittest.TestCase):
self.assertEqual(parsed.package_id, "author.mod") self.assertEqual(parsed.package_id, "author.mod")
self.assertEqual(parsed.display_name, "A & B") self.assertEqual(parsed.display_name, "A & B")
def test_steamcmd_runner_prepares_writable_workshop_path(self) -> None:
with tempfile.TemporaryDirectory() as directory:
content_dir = (
Path(directory)
/ "steamapps"
/ "workshop"
/ "content"
/ "294100"
)
runner = SteamCmdRunner("steamcmd", content_dir, 60)
runner.ensure_workshop_writable()
self.assertTrue(content_dir.is_dir())
self.assertFalse(
(content_dir.parent.parent / ".progression-write-test").exists()
)
def test_rml_uses_workshop_id_and_escapes_names(self) -> None: def test_rml_uses_workshop_id_and_escapes_names(self) -> None:
payload = generate_rml( payload = generate_rml(
[ [