reduce production image size without runtime drift
Some checks failed
Upload latest backend image to GHCR / upload (push) Failing after 2m45s
Test Backend / Build and Test Backend (push) Failing after 2m3s
Upload latest frontend image to GHCR / upload (push) Failing after 13s
Test Frontend / Build and Test Frontend (push) Successful in 10m51s
Trivy Security Scans / Trivy Filesystem Scan (Source Code) (push) Failing after 1m43s
Trivy Security Scans / Trivy Docker Image Scan (Backend & Frontend) (push) Failing after 27s

This commit is contained in:
alex wiesner
2026-03-16 15:07:36 +00:00
parent f24aa53575
commit 7a53cc2cc7
6 changed files with 119 additions and 103 deletions

32
backend/.dockerignore Normal file
View File

@@ -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.*

View File

@@ -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"]

View File

@@ -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}"
eval "value=\${$var:-}"
if [ -n "$value" ]; then
echo "$value"
return
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)
PGPASSWORD="$db_pass" psql -h "$db_host" -U "$db_user" -d "$db_name" -c '\q' >/dev/null 2>&1
"${VIRTUAL_ENV:-/opt/venv}/bin/python" - "$db_host" "$db_port" "$db_user" "$db_name" "$db_pass" <<'PY' >/dev/null 2>&1
import sys
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

View File

@@ -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

View File

@@ -2,3 +2,8 @@ node_modules
.svelte-kit
build
.env
.git
.github
.vscode
coverage
package-lock.json

View File

@@ -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"]