File size: 7,412 Bytes
1724d6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48447bb
1724d6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c250fa5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# ╔══════════════════════════════════════════════════════════════════════════════╗
# β•‘              disaster_grid β€” Hugging Face Spaces Dockerfile                β•‘
# β•‘  Target  : Hugging Face Spaces "Blank Docker" template                     β•‘
# β•‘  Runtime : Python 3.10-slim Β· FastAPI Β· Uvicorn Β· openenv-core             β•‘
# β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

# ── Stage 1: Base image ────────────────────────────────────────────────────────
# python:3.10-slim is chosen over the full image (~900 MB) because our RL
# environment has no C-extension build requirements that need a compiler at
# runtime.  This keeps the final layer under ~400 MB, which matters for cold-
# start latency on Hugging Face Spaces free-tier hardware.
FROM python:3.10-slim

# ── Stage 2: System dependencies ──────────────────────────────────────────────
# We install only what is strictly required:
#   curl  β€” health-check probes from the Spaces infra ping /health
#   build-essential β€” needed if any transitive dep compiles a C extension
# The apt cache is purged in the same RUN layer to avoid bloating the image
# with package index files (~30 MB on slim images).
RUN apt-get update && apt-get install -y --no-install-recommends \
        curl \
        build-essential \
    && rm -rf /var/lib/apt/lists/*

# ── Stage 3: Non-root user ─────────────────────────────────────────────────────
# CRITICAL (Hugging Face requirement):
# Hugging Face Spaces executes containers as a non-root user with uid=1000.
# If the image does not pre-create this user, the runtime uid has no entry in
# /etc/passwd, which breaks libraries that call getpwuid() (e.g. huggingface_hub,
# transformers' caching layer).
#
# We create:
#   - group "user"  gid=1000
#   - user  "user"  uid=1000, home=/home/user, no password, no sudo
#
# --no-log-init suppresses large sparse-file writes on some kernels.
RUN groupadd --gid 1000 user \
    && useradd --uid 1000 --gid 1000 --no-log-init --create-home user

# ── Stage 4: Working directory + ownership ─────────────────────────────────────
# CRITICAL (Hugging Face requirement):
# The /app directory must be owned by uid=1000 BEFORE we switch users.
# If we create /app as root and then switch to user, any write at runtime
# (e.g. openenv writing episode logs, Pydantic schema cache) will raise
# PermissionError.  chown here costs nothing at build time and prevents an
# entire class of runtime failures.
WORKDIR /app
RUN chown -R user:user /app

# ── Stage 5: PATH β€” expose local pip binaries ──────────────────────────────────
# CRITICAL (Hugging Face requirement):
# When pip installs packages as a non-root user it places console-script
# entry points (e.g. `uvicorn`, `openenv`) in ~/.local/bin, NOT in /usr/bin.
# Without this PATH addition the CMD instruction cannot find `uvicorn` and
# the container exits immediately with "exec: uvicorn: not found".
ENV PATH="/home/user/.local/bin:${PATH}"

# ── Stage 6: Switch to non-root user ──────────────────────────────────────────
# All subsequent RUN, COPY, and CMD instructions execute as uid=1000.
# Placing USER before COPY means the copied files are owned by user, not root,
# matching what the Spaces runtime expects.
USER user

# ── Stage 7: Install Python dependencies ──────────────────────────────────────
# We copy pyproject.toml first (before the rest of the source) to exploit
# Docker's layer cache: as long as pyproject.toml is unchanged, Docker will
# reuse the pip-install layer even when app.py or src/ is edited.  This
# reduces incremental build time from ~3 min to ~10 s on typical CI runners.
#
# --no-cache-dir  : prevents pip from writing the HTTP cache to disk
#                   (~200 MB saved in the image layer)
# --user          : installs into ~/.local (required since we are not root)
COPY --chown=user:user . /app
RUN pip install --no-cache-dir --user .

# ── Stage 8: Copy project source ──────────────────────────────────────────────
# Copied after the dependency install layer so that editing src/ or app.py
# does not invalidate the expensive pip install cache step above.
COPY --chown=user:user . /app

# ── Stage 9: Expose port ───────────────────────────────────────────────────────
# CRITICAL (Hugging Face requirement):
# Hugging Face Spaces routes all external HTTPS traffic to internal port 7860.
# Using any other port (e.g. 8000, 8080) means the Space will start but will
# return a 502 Bad Gateway for every request, with no error message in the
# build logs β€” one of the most confusing HF deployment failure modes.
EXPOSE 7860

# ── Stage 10: Health check ─────────────────────────────────────────────────────
# Hugging Face Spaces monitors container liveness via HTTP.  This HEALTHCHECK
# tells Docker (and the Spaces infra) that the app is ready once the /health
# endpoint responds with 2xx.  The 30 s start-period gives Uvicorn time to
# load the openenv environment before the first probe fires.
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
    CMD curl --fail --silent http://localhost:7860/health || exit 1

# ── Stage 11: Default command ──────────────────────────────────────────────────
# Uvicorn is launched in exec form (JSON array) rather than shell form so that
# SIGTERM from the Spaces orchestrator is delivered directly to the Uvicorn
# process, enabling graceful shutdown instead of a SIGKILL after the timeout.
#
# --host 0.0.0.0  : CRITICAL β€” binds to all interfaces so the Spaces proxy
#                   can reach the container.  127.0.0.1 or localhost would
#                   make the app invisible to the outside world.
# --port 7860     : must match the EXPOSE directive and the Spaces requirement.
# --workers 1     : single worker is safer for stateful RL environments;
#                   increase to 2+ only after verifying episode state is not
#                   shared across worker processes.
CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]