# Dockerfile.dev # ============================================================================ # DEVELOPMENT DOCKERFILE # ============================================================================ # This Dockerfile creates a development environment that matches production # as closely as possible while providing the tools needed for development. # # Base: Ubuntu 22.04 (LTS) - matches production server # Node: v20.x (LTS) - matches production # Includes: PostgreSQL client, Redis CLI, build tools, Bugsink, Logstash # ============================================================================ FROM ubuntu:22.04 # Set environment variables to non-interactive to avoid prompts during installation ENV DEBIAN_FRONTEND=noninteractive # ============================================================================ # Install System Dependencies # ============================================================================ # - curl: for downloading Node.js setup script and health checks # - git: for version control operations # - build-essential: for compiling native Node.js modules (node-gyp) # - python3, python3-pip, python3-venv: for Bugsink # - postgresql-client: for psql CLI (database initialization) # - redis-tools: for redis-cli (health checks) # - gnupg, apt-transport-https: for Elastic APT repository (Logstash) # - openjdk-17-jre-headless: required by Logstash # - nginx: for proxying Vite dev server with HTTPS # - libnss3-tools: required by mkcert for installing CA certificates # - wget: for downloading mkcert binary # - tzdata: timezone data required by Bugsink/Django (uses Europe/Amsterdam) RUN apt-get update && apt-get install -y \ curl \ git \ build-essential \ python3 \ python3-pip \ python3-venv \ postgresql-client \ redis-tools \ gnupg \ apt-transport-https \ openjdk-17-jre-headless \ nginx \ libnss3-tools \ wget \ tzdata \ && rm -rf /var/lib/apt/lists/* # ============================================================================ # Install Node.js 20.x (LTS) # ============================================================================ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y nodejs # ============================================================================ # Install PM2 Globally (ADR-014 Parity) # ============================================================================ # Install PM2 to match production architecture. This allows dev container to # run the same process management as production (cluster mode, workers, etc.) RUN npm install -g pm2 # ============================================================================ # Install mkcert and Generate Self-Signed Certificates # ============================================================================ # mkcert creates locally-trusted development certificates # This matches production HTTPS setup but with self-signed certs for localhost RUN wget -O /usr/local/bin/mkcert https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64 \ && chmod +x /usr/local/bin/mkcert # Create certificates directory and generate localhost certificates # ============================================================================ # IMPORTANT: Certificate includes MULTIPLE hostnames (SANs) # ============================================================================ # The certificate is generated for 'localhost', '127.0.0.1', AND '::1' because: # # 1. Users may access the site via https://localhost/ OR https://127.0.0.1/ # 2. Database stores image URLs using one hostname (typically 127.0.0.1) # 3. The seed script uses https://127.0.0.1 for image URLs (database constraint) # 4. NGINX is configured to accept BOTH hostnames (see docker/nginx/dev.conf) # # Without all hostnames in the certificate's Subject Alternative Names (SANs), # browsers would show ERR_CERT_AUTHORITY_INVALID when loading images or other # resources that use a different hostname than the one in the address bar. # # The mkcert command below creates a certificate valid for all three: # - localhost (IPv4 hostname) # - 127.0.0.1 (IPv4 address) # - ::1 (IPv6 loopback) # # See also: docker/nginx/dev.conf, docs/FLYER-URL-CONFIGURATION.md # ============================================================================ RUN mkdir -p /app/certs \ && cd /app/certs \ && mkcert -install \ && mkcert localhost 127.0.0.1 ::1 \ && mv localhost+2.pem localhost.crt \ && mv localhost+2-key.pem localhost.key # ============================================================================ # Install Logstash (Elastic APT Repository) # ============================================================================ # ADR-015: Log aggregation for Pino and Redis logs → Bugsink RUN curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | gpg --dearmor -o /usr/share/keyrings/elastic-keyring.gpg \ && echo "deb [signed-by=/usr/share/keyrings/elastic-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" | tee /etc/apt/sources.list.d/elastic-8.x.list \ && apt-get update \ && apt-get install -y logstash \ && rm -rf /var/lib/apt/lists/* # ============================================================================ # Install Bugsink (Python Package) # ============================================================================ # ADR-015: Self-hosted Sentry-compatible error tracking # Create a virtual environment for Bugsink to avoid conflicts RUN python3 -m venv /opt/bugsink \ && /opt/bugsink/bin/pip install --upgrade pip \ && /opt/bugsink/bin/pip install bugsink gunicorn psycopg2-binary # Create Bugsink directories and configuration RUN mkdir -p /var/log/bugsink /var/lib/bugsink /opt/bugsink/conf # Create Bugsink configuration file (Django settings module) # This file is imported by bugsink-manage via DJANGO_SETTINGS_MODULE # Based on bugsink/conf_templates/docker.py.template but customized for our setup RUN echo 'import os\n\ from urllib.parse import urlparse\n\ \n\ from bugsink.settings.default import *\n\ from bugsink.settings.default import DATABASES, SILENCED_SYSTEM_CHECKS\n\ from bugsink.conf_utils import deduce_allowed_hosts, deduce_script_name\n\ \n\ IS_DOCKER = True\n\ \n\ # Security settings\n\ SECRET_KEY = os.getenv("SECRET_KEY")\n\ DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "yes")\n\ \n\ # Silence cookie security warnings for dev (no HTTPS)\n\ SILENCED_SYSTEM_CHECKS += ["security.W012", "security.W016"]\n\ \n\ # Database configuration from DATABASE_URL environment variable\n\ if os.getenv("DATABASE_URL"):\n\ DATABASE_URL = os.getenv("DATABASE_URL")\n\ parsed = urlparse(DATABASE_URL)\n\ \n\ if parsed.scheme in ["postgres", "postgresql"]:\n\ DATABASES["default"] = {\n\ "ENGINE": "django.db.backends.postgresql",\n\ "NAME": parsed.path.lstrip("/"),\n\ "USER": parsed.username,\n\ "PASSWORD": parsed.password,\n\ "HOST": parsed.hostname,\n\ "PORT": parsed.port or "5432",\n\ }\n\ \n\ # Snappea (background task runner) settings\n\ SNAPPEA = {\n\ "TASK_ALWAYS_EAGER": False,\n\ "WORKAHOLIC": True,\n\ "NUM_WORKERS": 2,\n\ "PID_FILE": None,\n\ }\n\ DATABASES["snappea"]["NAME"] = "/tmp/snappea.sqlite3"\n\ \n\ # Site settings\n\ _PORT = os.getenv("PORT", "8000")\n\ BUGSINK = {\n\ "BASE_URL": os.getenv("BASE_URL", f"http://localhost:{_PORT}"),\n\ "SITE_TITLE": os.getenv("SITE_TITLE", "Flyer Crawler Error Tracking"),\n\ "SINGLE_USER": os.getenv("SINGLE_USER", "True").lower() in ("true", "1", "yes"),\n\ "SINGLE_TEAM": os.getenv("SINGLE_TEAM", "True").lower() in ("true", "1", "yes"),\n\ "PHONEHOME": False,\n\ }\n\ \n\ ALLOWED_HOSTS = deduce_allowed_hosts(BUGSINK["BASE_URL"])\n\ # Also allow 127.0.0.1 access (both localhost and 127.0.0.1 should work)\n\ if "127.0.0.1" not in ALLOWED_HOSTS:\n\ ALLOWED_HOSTS.append("127.0.0.1")\n\ if "localhost" not in ALLOWED_HOSTS:\n\ ALLOWED_HOSTS.append("localhost")\n\ \n\ # CSRF Trusted Origins (Django 4.0+ requires full origin for HTTPS POST requests)\n\ # This fixes "CSRF verification failed" errors when accessing Bugsink via HTTPS\n\ # Both localhost and 127.0.0.1 must be trusted to support different access patterns\n\ CSRF_TRUSTED_ORIGINS = [\n\ "https://localhost:8443",\n\ "https://127.0.0.1:8443",\n\ "http://localhost:8000",\n\ "http://127.0.0.1:8000",\n\ ]\n\ \n\ # Console email backend for dev\n\ EMAIL_BACKEND = "bugsink.email_backends.QuietConsoleEmailBackend"\n\ \n\ # HTTPS proxy support (nginx reverse proxy on port 8443)\n\ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")\n\ ' > /opt/bugsink/conf/bugsink_conf.py # Create Bugsink startup script # Uses DATABASE_URL environment variable (standard Docker approach per docs) RUN echo '#!/bin/bash\n\ set -e\n\ \n\ # Build DATABASE_URL from individual env vars for flexibility\n\ export DATABASE_URL="postgresql://${BUGSINK_DB_USER:-bugsink}:${BUGSINK_DB_PASSWORD:-bugsink_dev_password}@${BUGSINK_DB_HOST:-postgres}:${BUGSINK_DB_PORT:-5432}/${BUGSINK_DB_NAME:-bugsink}"\n\ # SECRET_KEY is required by Bugsink/Django\n\ export SECRET_KEY="${BUGSINK_SECRET_KEY:-dev-bugsink-secret-key-minimum-50-characters-for-security}"\n\ \n\ # Create superuser if not exists (for dev convenience)\n\ if [ -n "$BUGSINK_ADMIN_EMAIL" ] && [ -n "$BUGSINK_ADMIN_PASSWORD" ]; then\n\ export CREATE_SUPERUSER="${BUGSINK_ADMIN_EMAIL}:${BUGSINK_ADMIN_PASSWORD}"\n\ fi\n\ \n\ # Wait for PostgreSQL to be ready\n\ until pg_isready -h ${BUGSINK_DB_HOST:-postgres} -p ${BUGSINK_DB_PORT:-5432} -U ${BUGSINK_DB_USER:-bugsink}; do\n\ echo "Waiting for PostgreSQL..."\n\ sleep 2\n\ done\n\ \n\ echo "PostgreSQL is ready. Starting Bugsink..."\n\ echo "DATABASE_URL: postgresql://${BUGSINK_DB_USER}:***@${BUGSINK_DB_HOST}:${BUGSINK_DB_PORT}/${BUGSINK_DB_NAME}"\n\ \n\ # Change to config directory so bugsink_conf.py can be found\n\ cd /opt/bugsink/conf\n\ \n\ # Run migrations\n\ echo "Running database migrations..."\n\ /opt/bugsink/bin/bugsink-manage migrate --noinput\n\ \n\ # Create superuser if CREATE_SUPERUSER is set (format: email:password)\n\ if [ -n "$CREATE_SUPERUSER" ]; then\n\ IFS=":" read -r ADMIN_EMAIL ADMIN_PASS <<< "$CREATE_SUPERUSER"\n\ /opt/bugsink/bin/bugsink-manage shell -c "\n\ from django.contrib.auth import get_user_model\n\ User = get_user_model()\n\ if not User.objects.filter(email='"'"'$ADMIN_EMAIL'"'"').exists():\n\ User.objects.create_superuser('"'"'$ADMIN_EMAIL'"'"', '"'"'$ADMIN_PASS'"'"')\n\ print('"'"'Superuser created'"'"')\n\ else:\n\ print('"'"'Superuser already exists'"'"')\n\ " || true\n\ fi\n\ \n\ # Start Bugsink with Gunicorn\n\ echo "Starting Gunicorn on port ${BUGSINK_PORT:-8000}..."\n\ exec /opt/bugsink/bin/gunicorn \\\n\ --bind 0.0.0.0:${BUGSINK_PORT:-8000} \\\n\ --workers ${BUGSINK_WORKERS:-2} \\\n\ --access-logfile - \\\n\ --error-logfile - \\\n\ bugsink.wsgi:application\n\ ' > /usr/local/bin/start-bugsink.sh \ && chmod +x /usr/local/bin/start-bugsink.sh # ============================================================================ # Copy Logstash Pipeline Configuration # ============================================================================ # ADR-015 + ADR-050: Multi-source log aggregation to Bugsink # Configuration file includes: # - Pino application logs (Backend API errors) # - PostgreSQL logs (including fn_log() structured output) # - NGINX access and error logs # See docker/logstash/bugsink.conf for full configuration RUN mkdir -p /etc/logstash/conf.d /app/logs COPY docker/logstash/bugsink.conf /etc/logstash/conf.d/bugsink.conf # Create Logstash directories RUN mkdir -p /var/lib/logstash && chown -R logstash:logstash /var/lib/logstash RUN mkdir -p /var/log/logstash && chown -R logstash:logstash /var/log/logstash # Create PM2 log directory (ADR-014 Parity) # Logs written here will be picked up by Logstash (ADR-050) RUN mkdir -p /var/log/pm2 # ============================================================================ # Configure Nginx # ============================================================================ # Copy development nginx configuration COPY docker/nginx/dev.conf /etc/nginx/sites-available/default # Configure nginx to run in foreground (required for container) RUN echo "daemon off;" >> /etc/nginx/nginx.conf # ============================================================================ # Set Working Directory # ============================================================================ WORKDIR /app # ============================================================================ # Install Node.js Dependencies # ============================================================================ # Copy package files first for better Docker layer caching COPY package*.json ./ # Install all dependencies (including devDependencies for development) RUN npm install # ============================================================================ # Environment Configuration # ============================================================================ # Default environment variables for development ENV NODE_ENV=development # Increase Node.js memory limit for large builds ENV NODE_OPTIONS='--max-old-space-size=8192' # Bugsink defaults (ADR-015) ENV BUGSINK_DB_HOST=postgres ENV BUGSINK_DB_PORT=5432 ENV BUGSINK_DB_NAME=bugsink ENV BUGSINK_DB_USER=bugsink ENV BUGSINK_DB_PASSWORD=bugsink_dev_password ENV BUGSINK_PORT=8000 ENV BUGSINK_BASE_URL=http://localhost:8000 ENV BUGSINK_ADMIN_EMAIL=admin@localhost ENV BUGSINK_ADMIN_PASSWORD=admin # ============================================================================ # Expose Ports # ============================================================================ # 80 - HTTP redirect to HTTPS (matches production) # 443 - Nginx HTTPS frontend proxy (Vite on 5173) # 3001 - Express backend # 8000 - Bugsink error tracking EXPOSE 80 443 3001 8000 # ============================================================================ # Copy Application Code and Scripts # ============================================================================ # Copy the scripts directory which contains the entrypoint script COPY scripts/ /app/scripts/ # ============================================================================ # Fix Line Endings for Windows Compatibility # ============================================================================ # Convert ALL text files from CRLF to LF (Windows to Unix) # This ensures compatibility when building on Windows hosts # We process: shell scripts, JS/TS files, JSON, config files, etc. RUN find /app -type f \( \ -name "*.sh" -o \ -name "*.js" -o \ -name "*.ts" -o \ -name "*.tsx" -o \ -name "*.jsx" -o \ -name "*.json" -o \ -name "*.conf" -o \ -name "*.config" -o \ -name "*.yml" -o \ -name "*.yaml" \ \) -exec sed -i 's/\r$//' {} \; && \ find /etc/nginx -type f -name "*.conf" -exec sed -i 's/\r$//' {} \; && \ chmod +x /app/scripts/*.sh # ============================================================================ # Default Command # ============================================================================ # Keep container running so VS Code can attach. # Actual commands (npm run dev, etc.) are run via devcontainer.json. CMD ["bash"]