diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..ad97d132 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,32 @@ +.git +.github +.devcontainer +.env +__pycache__/ +*.py[cod] +*.log +*.sqlite3 +.coverage +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +coverage.xml +htmlcov/ +server/.pytest_cache +server/.mypy_cache +server/.ruff_cache +server/.coverage +server/coverage.xml +server/htmlcov +server/.hypothesis +server/scheduler.log +server/**/*.log +server/tests/ +server/**/tests/ +server/test_*.py +server/**/__pycache__/ +server/**/*.py[cod] +server/media/ +server/staticfiles/ +server/.env +server/.env.* diff --git a/backend/Dockerfile b/backend/Dockerfile index 98199d17..ffc30192 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,8 +1,7 @@ -# Stage 1: Build stage with dependencies ARG PYTHON_IMAGE=python:3.13-slim + FROM ${PYTHON_IMAGE} AS builder -# Metadata labels LABEL maintainer="Voyage contributors" \ version="0.10.0" \ description="Voyage — the ultimate self-hosted travel companion." \ @@ -14,64 +13,55 @@ LABEL maintainer="Voyage contributors" \ org.opencontainers.image.vendor="Voyage contributors" \ org.opencontainers.image.licenses="GPL-3.0" -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + DEBIAN_FRONTEND=noninteractive \ + VIRTUAL_ENV=/opt/venv + WORKDIR /code -ENV DEBIAN_FRONTEND=noninteractive -# Install system dependencies needed for build RUN apt-get update && apt-get install -y --no-install-recommends \ - git \ - postgresql-client \ - gdal-bin \ + build-essential \ libgdal-dev \ - nginx \ - memcached \ - supervisor \ - && apt-get clean && rm -rf /var/lib/apt/lists/* + libpq-dev \ + && rm -rf /var/lib/apt/lists/* -# Install Python dependencies -COPY ./server/requirements.txt /code/ -RUN pip install --upgrade pip \ - && pip install --no-cache-dir -r requirements.txt +COPY ./server/requirements.txt /tmp/requirements.txt + +RUN python -m venv "$VIRTUAL_ENV" \ + && "$VIRTUAL_ENV/bin/pip" install --upgrade pip \ + && "$VIRTUAL_ENV/bin/pip" install --no-cache-dir --no-compile --prefer-binary -r /tmp/requirements.txt \ + && find "$VIRTUAL_ENV" \( -type d -name '__pycache__' -o -type d -name 'tests' \) -prune -exec rm -rf '{}' + \ + && find "$VIRTUAL_ENV" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete -# Stage 2: Final image with runtime dependencies FROM ${PYTHON_IMAGE} -WORKDIR /code -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 -ENV DEBIAN_FRONTEND=noninteractive -# Install runtime dependencies (including GDAL) +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + DEBIAN_FRONTEND=noninteractive \ + VIRTUAL_ENV=/opt/venv + +WORKDIR /code + RUN apt-get update && apt-get install -y --no-install-recommends \ - postgresql-client \ - gdal-bin \ - libgdal-dev \ + libgdal36 \ nginx \ memcached \ supervisor \ - && apt-get clean && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* -# Copy Python packages from builder -COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages -COPY --from=builder /usr/local/bin /usr/local/bin - -# Copy project code and configs +COPY --from=builder /opt/venv /opt/venv COPY ./server /code/ COPY ./nginx.conf /etc/nginx/nginx.conf COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY ./entrypoint.sh /code/entrypoint.sh + RUN chmod +x /code/entrypoint.sh \ && mkdir -p /code/static /code/media -# Collect static files -RUN python3 manage.py collectstatic --noinput --verbosity 2 +RUN "$VIRTUAL_ENV/bin/python" manage.py collectstatic --noinput --verbosity 2 -# Expose ports EXPOSE 80 8000 -# Start with an entrypoint that runs init tasks then starts supervisord ENTRYPOINT ["/code/entrypoint.sh"] - -# Start supervisord to manage processes CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index eef4e40e..bfc43ca0 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -1,29 +1,45 @@ -#!/bin/bash +#!/bin/sh # Function to check PostgreSQL availability # Helper to get the first non-empty environment variable get_env() { - for var in "$@"; do - value="${!var}" - if [ -n "$value" ]; then - echo "$value" - return - fi - done + for var in "$@"; do + eval "value=\${$var:-}" + if [ -n "$value" ]; then + printf '%s\n' "$value" + return 0 + fi + done + + return 1 } check_postgres() { - local db_host - local db_user - local db_name - local db_pass + db_host=$(get_env PGHOST) + db_host=${db_host:-localhost} + db_port=$(get_env PGPORT) + db_port=${db_port:-5432} + db_user=$(get_env PGUSER POSTGRES_USER) + db_name=$(get_env PGDATABASE POSTGRES_DB) + db_pass=$(get_env PGPASSWORD POSTGRES_PASSWORD) - db_host=$(get_env PGHOST) - db_user=$(get_env PGUSER POSTGRES_USER) - db_name=$(get_env PGDATABASE POSTGRES_DB) - db_pass=$(get_env PGPASSWORD POSTGRES_PASSWORD) + "${VIRTUAL_ENV:-/opt/venv}/bin/python" - "$db_host" "$db_port" "$db_user" "$db_name" "$db_pass" <<'PY' >/dev/null 2>&1 +import sys - PGPASSWORD="$db_pass" psql -h "$db_host" -U "$db_user" -d "$db_name" -c '\q' >/dev/null 2>&1 +import psycopg2 + +host, port, user, dbname, password = sys.argv[1:] + +conn = psycopg2.connect( + host=host, + port=int(port), + user=user, + dbname=dbname, + password=password, + connect_timeout=1, +) +conn.close() +PY } @@ -39,12 +55,12 @@ done # psql -h "$PGHOST" -U "$PGUSER" -d "$PGDATABASE" -f /app/backend/init-postgis.sql # Apply Django migrations -python manage.py migrate +${VIRTUAL_ENV:-/opt/venv}/bin/python manage.py migrate # Create superuser if environment variables are set and there are no users present at all. if [ -n "$DJANGO_ADMIN_USERNAME" ] && [ -n "$DJANGO_ADMIN_PASSWORD" ] && [ -n "$DJANGO_ADMIN_EMAIL" ]; then echo "Creating superuser..." - python manage.py shell << EOF + ${VIRTUAL_ENV:-/opt/venv}/bin/python manage.py shell << EOF from django.contrib.auth import get_user_model from allauth.account.models import EmailAddress @@ -76,7 +92,7 @@ fi # Sync the countries and world travel regions # Sync the countries and world travel regions -python manage.py download-countries +${VIRTUAL_ENV:-/opt/venv}/bin/python manage.py download-countries if [ $? -eq 137 ]; then >&2 echo "WARNING: The download-countries command was interrupted. This is likely due to lack of memory allocated to the container or the host. Please try again with more memory." exit 1 @@ -84,4 +100,4 @@ fi cat /code/voyage.txt -exec "$@" \ No newline at end of file +exec "$@" diff --git a/backend/supervisord.conf b/backend/supervisord.conf index eb72171b..78ed87be 100644 --- a/backend/supervisord.conf +++ b/backend/supervisord.conf @@ -8,7 +8,7 @@ stdout_logfile=/dev/stdout stderr_logfile=/dev/stderr [program:gunicorn] -command=/usr/local/bin/gunicorn main.wsgi:application --bind [::]:8000 --workers 2 --timeout 120 +command=/opt/venv/bin/gunicorn main.wsgi:application --bind [::]:8000 --workers 2 --timeout 120 directory=/code autorestart=true stdout_logfile=/dev/stdout @@ -25,7 +25,7 @@ stdout_logfile_maxbytes=0 stderr_logfile_maxbytes=0 [program:sync_visited_regions] -command=/usr/local/bin/python3 /code/run_periodic_sync.py +command=/opt/venv/bin/python /code/run_periodic_sync.py directory=/code autorestart=true stdout_logfile=/dev/stdout diff --git a/frontend/.dockerignore b/frontend/.dockerignore index 18774d63..f1f1338f 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -2,3 +2,8 @@ node_modules .svelte-kit build .env +.git +.github +.vscode +coverage +package-lock.json diff --git a/frontend/Dockerfile b/frontend/Dockerfile index c494e60b..0ecc9e97 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,8 +1,7 @@ -# Use the official Bun image as the build platform (supports linux/amd64 and linux/arm64 natively) ARG BUN_VERSION=1.3.10 -FROM oven/bun:${BUN_VERSION}-alpine AS builder -# Metadata labels for the Voyage image +FROM oven/bun:${BUN_VERSION} AS deps + LABEL maintainer="Voyage contributors" \ version="v0.12.0" \ description="Voyage — the ultimate self-hosted travel companion." \ @@ -13,60 +12,34 @@ LABEL maintainer="Voyage contributors" \ org.opencontainers.image.url="https://voyage.app" \ org.opencontainers.image.source="https://github.com/Alex-Wiesner/voyage" \ org.opencontainers.image.vendor="Voyage contributors" \ - org.opencontainers.image.created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ org.opencontainers.image.licenses="GPL-3.0" -# The WORKDIR instruction sets the working directory for everything that will happen next WORKDIR /app -# Upgrade zlib to include Alpine security fixes -RUN apk upgrade --no-cache zlib - -# Copy package files first for better Docker layer caching COPY package.json bun.lock* ./ - -# Clean install all node modules using bun with frozen lockfile RUN bun install --frozen-lockfile -# Copy the rest of the application files +FROM deps AS builder + COPY . . +RUN rm -f .env \ + && bun run build -# Remove the development .env file if present -RUN rm -f .env - -# Build SvelteKit app -RUN bun run build - -# Make startup script executable -RUN chmod +x ./startup.sh - -# Keep only production dependencies for runtime image -RUN bun install --frozen-lockfile --production - -# Runtime image contains only built app + runtime deps -FROM node:22-alpine AS runtime +FROM oven/bun:${BUN_VERSION} AS prod-deps WORKDIR /app -# Upgrade zlib and remove npm toolchain from runtime image -RUN apk upgrade --no-cache zlib \ - && rm -f /usr/local/bin/npm /usr/local/bin/npx \ - && rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack +COPY package.json bun.lock* ./ +RUN bun install --frozen-lockfile --production + +FROM gcr.io/distroless/nodejs22-debian12:nonroot AS runtime + +WORKDIR /app -# Copy build artifacts and production runtime dependencies COPY --from=builder /app/build ./build -COPY --from=builder /app/node_modules ./node_modules +COPY --from=prod-deps /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json -COPY --from=builder /app/startup.sh ./startup.sh -# Ensure startup script is executable -RUN chmod +x ./startup.sh - -# Change to non-root user for security -USER node:node - -# Expose the port that the app is listening on EXPOSE 3000 -# Run startup.sh instead of the default command -CMD ["./startup.sh"] +CMD ["build/index.js"]