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
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:
32
backend/.dockerignore
Normal file
32
backend/.dockerignore
Normal 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.*
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
# Stage 1: Build stage with dependencies
|
|
||||||
ARG PYTHON_IMAGE=python:3.13-slim
|
ARG PYTHON_IMAGE=python:3.13-slim
|
||||||
|
|
||||||
FROM ${PYTHON_IMAGE} AS builder
|
FROM ${PYTHON_IMAGE} AS builder
|
||||||
|
|
||||||
# Metadata labels
|
|
||||||
LABEL maintainer="Voyage contributors" \
|
LABEL maintainer="Voyage contributors" \
|
||||||
version="0.10.0" \
|
version="0.10.0" \
|
||||||
description="Voyage — the ultimate self-hosted travel companion." \
|
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.vendor="Voyage contributors" \
|
||||||
org.opencontainers.image.licenses="GPL-3.0"
|
org.opencontainers.image.licenses="GPL-3.0"
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
ENV PYTHONUNBUFFERED=1
|
PYTHONUNBUFFERED=1 \
|
||||||
|
DEBIAN_FRONTEND=noninteractive \
|
||||||
|
VIRTUAL_ENV=/opt/venv
|
||||||
|
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
# Install system dependencies needed for build
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
git \
|
build-essential \
|
||||||
postgresql-client \
|
|
||||||
gdal-bin \
|
|
||||||
libgdal-dev \
|
libgdal-dev \
|
||||||
nginx \
|
libpq-dev \
|
||||||
memcached \
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
supervisor \
|
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Python dependencies
|
COPY ./server/requirements.txt /tmp/requirements.txt
|
||||||
COPY ./server/requirements.txt /code/
|
|
||||||
RUN pip install --upgrade pip \
|
RUN python -m venv "$VIRTUAL_ENV" \
|
||||||
&& pip install --no-cache-dir -r requirements.txt
|
&& "$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}
|
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 \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
postgresql-client \
|
libgdal36 \
|
||||||
gdal-bin \
|
|
||||||
libgdal-dev \
|
|
||||||
nginx \
|
nginx \
|
||||||
memcached \
|
memcached \
|
||||||
supervisor \
|
supervisor \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy Python packages from builder
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
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 ./server /code/
|
COPY ./server /code/
|
||||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||||
COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
COPY ./entrypoint.sh /code/entrypoint.sh
|
COPY ./entrypoint.sh /code/entrypoint.sh
|
||||||
|
|
||||||
RUN chmod +x /code/entrypoint.sh \
|
RUN chmod +x /code/entrypoint.sh \
|
||||||
&& mkdir -p /code/static /code/media
|
&& mkdir -p /code/static /code/media
|
||||||
|
|
||||||
# Collect static files
|
RUN "$VIRTUAL_ENV/bin/python" manage.py collectstatic --noinput --verbosity 2
|
||||||
RUN python3 manage.py collectstatic --noinput --verbosity 2
|
|
||||||
|
|
||||||
# Expose ports
|
|
||||||
EXPOSE 80 8000
|
EXPOSE 80 8000
|
||||||
|
|
||||||
# Start with an entrypoint that runs init tasks then starts supervisord
|
|
||||||
ENTRYPOINT ["/code/entrypoint.sh"]
|
ENTRYPOINT ["/code/entrypoint.sh"]
|
||||||
|
|
||||||
# Start supervisord to manage processes
|
|
||||||
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
|
|||||||
@@ -1,29 +1,45 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
|
|
||||||
# Function to check PostgreSQL availability
|
# Function to check PostgreSQL availability
|
||||||
# Helper to get the first non-empty environment variable
|
# Helper to get the first non-empty environment variable
|
||||||
get_env() {
|
get_env() {
|
||||||
for var in "$@"; do
|
for var in "$@"; do
|
||||||
value="${!var}"
|
eval "value=\${$var:-}"
|
||||||
if [ -n "$value" ]; then
|
if [ -n "$value" ]; then
|
||||||
echo "$value"
|
printf '%s\n' "$value"
|
||||||
return
|
return 0
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
check_postgres() {
|
check_postgres() {
|
||||||
local db_host
|
db_host=$(get_env PGHOST)
|
||||||
local db_user
|
db_host=${db_host:-localhost}
|
||||||
local db_name
|
db_port=$(get_env PGPORT)
|
||||||
local db_pass
|
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)
|
"${VIRTUAL_ENV:-/opt/venv}/bin/python" - "$db_host" "$db_port" "$db_user" "$db_name" "$db_pass" <<'PY' >/dev/null 2>&1
|
||||||
db_user=$(get_env PGUSER POSTGRES_USER)
|
import sys
|
||||||
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
|
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
|
# psql -h "$PGHOST" -U "$PGUSER" -d "$PGDATABASE" -f /app/backend/init-postgis.sql
|
||||||
|
|
||||||
# Apply Django migrations
|
# 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.
|
# 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
|
if [ -n "$DJANGO_ADMIN_USERNAME" ] && [ -n "$DJANGO_ADMIN_PASSWORD" ] && [ -n "$DJANGO_ADMIN_EMAIL" ]; then
|
||||||
echo "Creating superuser..."
|
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 django.contrib.auth import get_user_model
|
||||||
from allauth.account.models import EmailAddress
|
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
|
||||||
# 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
|
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."
|
>&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
|
exit 1
|
||||||
@@ -84,4 +100,4 @@ fi
|
|||||||
|
|
||||||
cat /code/voyage.txt
|
cat /code/voyage.txt
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ stdout_logfile=/dev/stdout
|
|||||||
stderr_logfile=/dev/stderr
|
stderr_logfile=/dev/stderr
|
||||||
|
|
||||||
[program:gunicorn]
|
[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
|
directory=/code
|
||||||
autorestart=true
|
autorestart=true
|
||||||
stdout_logfile=/dev/stdout
|
stdout_logfile=/dev/stdout
|
||||||
@@ -25,7 +25,7 @@ stdout_logfile_maxbytes=0
|
|||||||
stderr_logfile_maxbytes=0
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
[program:sync_visited_regions]
|
[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
|
directory=/code
|
||||||
autorestart=true
|
autorestart=true
|
||||||
stdout_logfile=/dev/stdout
|
stdout_logfile=/dev/stdout
|
||||||
|
|||||||
@@ -2,3 +2,8 @@ node_modules
|
|||||||
.svelte-kit
|
.svelte-kit
|
||||||
build
|
build
|
||||||
.env
|
.env
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.vscode
|
||||||
|
coverage
|
||||||
|
package-lock.json
|
||||||
|
|||||||
@@ -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
|
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" \
|
LABEL maintainer="Voyage contributors" \
|
||||||
version="v0.12.0" \
|
version="v0.12.0" \
|
||||||
description="Voyage — the ultimate self-hosted travel companion." \
|
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.url="https://voyage.app" \
|
||||||
org.opencontainers.image.source="https://github.com/Alex-Wiesner/voyage" \
|
org.opencontainers.image.source="https://github.com/Alex-Wiesner/voyage" \
|
||||||
org.opencontainers.image.vendor="Voyage contributors" \
|
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"
|
org.opencontainers.image.licenses="GPL-3.0"
|
||||||
|
|
||||||
# The WORKDIR instruction sets the working directory for everything that will happen next
|
|
||||||
WORKDIR /app
|
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* ./
|
COPY package.json bun.lock* ./
|
||||||
|
|
||||||
# Clean install all node modules using bun with frozen lockfile
|
|
||||||
RUN bun install --frozen-lockfile
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
# Copy the rest of the application files
|
FROM deps AS builder
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN rm -f .env \
|
||||||
|
&& bun run build
|
||||||
|
|
||||||
# Remove the development .env file if present
|
FROM oven/bun:${BUN_VERSION} AS prod-deps
|
||||||
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
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Upgrade zlib and remove npm toolchain from runtime image
|
COPY package.json bun.lock* ./
|
||||||
RUN apk upgrade --no-cache zlib \
|
RUN bun install --frozen-lockfile --production
|
||||||
&& rm -f /usr/local/bin/npm /usr/local/bin/npx \
|
|
||||||
&& rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack
|
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/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/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
|
EXPOSE 3000
|
||||||
|
|
||||||
# Run startup.sh instead of the default command
|
CMD ["build/index.js"]
|
||||||
CMD ["./startup.sh"]
|
|
||||||
|
|||||||
Reference in New Issue
Block a user