# pull official base image FROM python:3.12-slim # create directory for the app user RUN mkdir -p /home/app # create the app user RUN addgroup --system app && adduser --system app && adduser app app # create the appropriate directories ENV HOME=/home/app ENV APP_HOME=/home/app/web/ RUN mkdir $APP_HOME WORKDIR $APP_HOME #install geo libs RUN apt-get -y update && apt-get -y upgrade RUN apt-get -y install apt-utils binutils libproj-dev gdal-bin postgresql-client # install dependencies RUN pip install pipenv COPY Pipfile Pipfile.lock $APP_HOME RUN pipenv install --system EXPOSE 8000 # copy entrypoint.sh and add execute permission COPY ./entrypoint.sh $APP_HOME RUN ["chmod", "u+x", "/home/app/web/entrypoint.sh"] # copy project COPY . $APP_HOME # Collect static assets into the image rather than at container start. Hashing and # compressing 500+ assets through CompressedManifestStaticFilesStorage took tens of # seconds on every Render start, inside the deploy's 502 window (the service mounts a # disk, so Render cannot run old and new instances side by side). It needs no secrets: # settings.py defaults SECRET_KEY, and collectstatic touches no database. # # Side benefit: a dangling {% static %} reference now fails the build instead of the # already-committed production deploy. # # Must stay above the chown, so the generated tree is owned by `app` like everything else. RUN python manage.py collectstatic --no-input # chown all the files to the app user RUN chown -R app:app $APP_HOME # Render SSH (and the dashboard Shell tab) exec a shell as `app` inside the # running container. `adduser --system` left the user on /usr/sbin/nologin, so # the gateway authenticated the key and then had nothing to exec — every # session ended with "Connection closed by remote host" before a single # command ran. Render's docs also require ~/.ssh (0700, owned by the running # user) to exist in the image. Both fixed here; without them the disk-to-S3 # migration cannot be run at all, since one-off jobs do not mount the disk. # usermod -d matters as much as -s: `adduser --system` recorded /nonexistent as # the home directory in passwd, and the SSH agent resolves ~/.ssh through # passwd, not through the HOME env var that papers over it for the app itself. RUN usermod -s /bin/bash -d /home/app app \ && mkdir -p /home/app/.ssh \ && chmod 700 /home/app/.ssh \ && chown app:app /home/app /home/app/.ssh # change to the app user USER app ENTRYPOINT ["/home/app/web/entrypoint.sh"] CMD gunicorn --bind :${PORT:-8000} --workers ${WEB_CONCURRENCY:-2} --threads ${GUNICORN_THREADS:-4} --worker-class ${GUNICORN_WORKER_CLASS:-mapsurvey.gunicorn_workers.DrainingThreadWorker} --timeout ${GUNICORN_TIMEOUT:-60} --max-requests ${GUNICORN_MAX_REQUESTS:-300} --max-requests-jitter ${GUNICORN_MAX_REQUESTS_JITTER:-60} mapsurvey.wsgi:application