diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..c3fe56265 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.git +.gitignore +.venv +venv* +__pycache__ +*.pyc +.DS_Store +*.egg-info +.cache +.idea +celerybeat-* +media/* +*.md +!CLAUDE.md +docs/ +selenium/ +deploy-staging.sh +reset.sh +settings_ci.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..58a463146 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# Dockerfile for Fly.io deployment +# Also usable for any Docker-based deployment. +# Heroku deployment continues to use the Procfile and does not use this file. + +FROM python:3.13-slim AS base + +# Install system dependencies required by python-ldap and psycopg2 +RUN apt-get update && apt-get install -y --no-install-recommends \ + libldap2-dev \ + libsasl2-dev \ + libpq-dev \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Install uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +WORKDIR /app + +# Install dependencies first (layer caching) +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen --no-dev + +# Copy application code +COPY . . + +# Default port (Fly.io sets PORT=8080 by default) +ENV PORT=8080 + +EXPOSE 8080 + +# Run gunicorn via uv so it picks up the virtual environment +CMD ["sh", "-c", "uv run gunicorn wsgi:application -b 0.0.0.0:$PORT -w 4"] diff --git a/fly.toml b/fly.toml new file mode 100644 index 000000000..8120ec5d5 --- /dev/null +++ b/fly.toml @@ -0,0 +1,57 @@ +# Fly.io deployment configuration for Helios Voting +# +# To deploy: +# 1. Install flyctl: https://fly.io/docs/flyctl/install/ +# 2. Create the app: fly apps create your-app-name +# 3. Create a Postgres database: fly postgres create +# 4. Attach it: fly postgres attach your-db-name +# 5. Set secrets: +# fly secrets set SECRET_KEY="your-secret-key" +# fly secrets set ALLOWED_HOSTS="your-app-name.fly.dev" +# fly secrets set URL_HOST="https://your-app-name.fly.dev" +# fly secrets set SSL=1 +# fly secrets set HSTS=1 +# fly secrets set DEBUG=0 +# 6. Deploy: fly deploy +# +# For background worker (Celery), scale a separate process group: +# fly scale count worker=1 +# +# Note: Fly Postgres uses internal networking without SSL, +# so DATABASE_SSL_REQUIRE defaults to 0 (override via env if needed). + +app = "helios-voting" +primary_region = "iad" + +[build] + +[env] + PORT = "8080" + # Fly Postgres connects over internal network without SSL + DATABASE_SSL_REQUIRE = "0" + +[processes] + web = "uv run gunicorn wsgi:application -b 0.0.0.0:8080 -w 4" + worker = "uv run celery --app helios worker --events --beat --concurrency 1" + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = "stop" + auto_start_machines = true + min_machines_running = 0 + processes = ["web"] + + [http_service.concurrency] + type = "connections" + hard_limit = 250 + soft_limit = 200 + +[[vm]] + memory = "512mb" + cpu_kind = "shared" + cpus = 1 + +[[statics]] + guest_path = "/app/server_ui/media" + url_prefix = "/static/" diff --git a/settings.py b/settings.py index 66caf46ff..44e48e0f0 100644 --- a/settings.py +++ b/settings.py @@ -52,7 +52,8 @@ def get_from_env(var, default): # override if we have an env variable if get_from_env('DATABASE_URL', None): import dj_database_url - DATABASES['default'] = dj_database_url.config(conn_max_age=600, ssl_require=True) + _db_ssl = get_from_env('DATABASE_SSL_REQUIRE', '1') == '1' + DATABASES['default'] = dj_database_url.config(conn_max_age=600, ssl_require=_db_ssl) DATABASES['default']['ENGINE'] = 'django.db.backends.postgresql' # explicitly set the default auto-created primary field to silence warning models.W042