Files
microfish/Dockerfile
Kunthawat Greethong 42208c4f5a fix: run alembic migrations before starting services
Production image had no DB migration step, so a fresh container had an
empty database: the durable worker queried the 'jobs' table before it
existed and crash-looped with sqlalchemy OperationalError 'no such table:
jobs' (supervisor restart loop).

- Add backend/docker_entrypoint.sh: fail-fast if DATABASE_URL is unset,
  run 'alembic upgrade head' (idempotent), then exec supervisord.
- Dockerfile CMD now runs the entrypoint.
- supervisor: fix nodaemon typo, stream stdout/stderr to /dev/stdout +
  /dev/stderr so worker errors are visible in container logs, and give
  worker startsecs/startretries.

Verified locally: entrypoint bash syntax ok, alembic upgrade head
idempotent, jobs + 19 tables created, worker --once exits 0 after migrate
(previously exit 1 with no-such-table). Worker/schema tests 11 passed.
2026-08-31 15:16:59 +07:00

66 lines
3.4 KiB
Docker

# ============================================================
# CrowdSight production image (multi-service, EasyPanel-buildable)
#
# Services inside one container (supervisord):
# - web: nginx serving the built SPA, proxying /api -> gunicorn :5001
# - backend: gunicorn WSGI (wsgi:app) on 0.0.0.0:5001
# - worker: durable PollingWorker (backend/worker.py)
# ============================================================
# ---- Stage 1: build the frontend SPA ----
FROM node:20 AS frontend-build
WORKDIR /build
COPY package.json package-lock.json* ./
COPY frontend/package.json frontend/package-lock.json* ./frontend/
# install root + frontend deps
RUN npm ci 2>/dev/null || true; npm ci --prefix frontend || true
COPY locales ./locales
COPY frontend ./frontend
RUN npm run build --prefix frontend
# ---- Stage 2: runtime (python + nginx + supervisord) ----
FROM python:3.11-slim AS runtime
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONPATH=/app/backend
RUN apt-get update \
&& apt-get install -y --no-install-recommends nginx supervisor \
&& rm -rf /var/lib/apt/lists/*
# uv runtime
COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /bin/
WORKDIR /app
# Install backend deps first (cache-friendly)
COPY backend/pyproject.toml backend/uv.lock ./backend/
RUN cd backend && uv sync --frozen --no-dev
# Copy project source
COPY backend ./backend
COPY locales ./locales
COPY package.json ./
# Make the migration-runner entrypoint executable
RUN chmod +x /app/backend/docker_entrypoint.sh
# Copy built SPA into nginx web root
COPY --from=frontend-build /build/frontend/dist /usr/share/nginx/html
# nginx config: SPA + /api proxy to gunicorn
RUN echo 'server {\n listen 8080;\n server_name _;\n root /usr/share/nginx/html;\n index index.html;\n location / { try_files $uri $uri/ /index.html; }\n location /api/ {\n proxy_pass http://127.0.0.1:5001;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n }\n}\n' > /etc/nginx/sites-available/crowdsight \
&& ln -sf /etc/nginx/sites-available/crowdsight /etc/nginx/sites-enabled/crowdsight \
&& rm -f /etc/nginx/sites-enabled/default
# supervisor: run nginx + gunicorn + worker (migrations already run by entrypoint)
RUN echo '[supervisord]\nnodaemon=true\nlogfile=/var/log/supervisor/supervisord.log\npidfile=/var/run/supervisord.pid\n\n[program:nginx]\ncommand=/usr/sbin/nginx -g "daemon off;"\nautostart=true\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:backend]\ncommand=/bin/bash -c "cd /app/backend && uv run gunicorn -w 2 -b 0.0.0.0:5001 --timeout 120 wsgi:app"\nautostart=true\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:worker]\ncommand=/bin/bash -c "cd /app/backend && uv run python worker.py --poll-interval 5"\nautostart=true\nautorestart=true\nstartsecs=2\nstartretries=5\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n' > /etc/supervisor/conf.d/crowdsight.conf
EXPOSE 8080 5001
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:5001/health', timeout=4)" || exit 1
CMD ["/app/backend/docker_entrypoint.sh"]