diff --git a/.dockerignore b/.dockerignore index 4448c2a7d88d1..2c26fc9cb0041 100644 --- a/.dockerignore +++ b/.dockerignore @@ -33,3 +33,4 @@ !plugin-server/.prettierrc !share/GeoLite2-City.mmdb !hogvm/python +!unit.json \ No newline at end of file diff --git a/.github/actions/build-n-cache-image/action.yml b/.github/actions/build-n-cache-image/action.yml index ce2bc45218322..d5ceb305fbcd3 100644 --- a/.github/actions/build-n-cache-image/action.yml +++ b/.github/actions/build-n-cache-image/action.yml @@ -35,3 +35,15 @@ runs: platforms: linux/amd64,linux/arm64 env: ACTIONS_ID_TOKEN_REQUEST_URL: ${{ inputs.actions-id-token-request-url }} + + - name: Build unit image + id: build-unit + uses: depot/build-push-action@v1 + with: + buildx-fallback: false # buildx is so slow it's better to just fail + load: ${{ inputs.load }} + file: production-unit.Dockerfile + tags: ${{ steps.emit.outputs.tag }} + platforms: linux/amd64 + env: + ACTIONS_ID_TOKEN_REQUEST_URL: ${{ inputs.actions-id-token-request-url }} diff --git a/.github/workflows/container-images-cd.yml b/.github/workflows/container-images-cd.yml index 05e9ada383988..10556e36c4c59 100644 --- a/.github/workflows/container-images-cd.yml +++ b/.github/workflows/container-images-cd.yml @@ -68,9 +68,19 @@ jobs: with: buildx-fallback: false # the fallback is so slow it's better to just fail push: true - tags: posthog/posthog:${{github.sha}},posthog/posthog:latest,${{ steps.aws-ecr.outputs.registry }}/posthog-cloud:master + tags: posthog/posthog:${{ github.sha }},posthog/posthog:latest,${{ steps.aws-ecr.outputs.registry }}/posthog-cloud:master platforms: linux/arm64,linux/amd64 + - name: Build and push unit container image + id: build-unit + uses: depot/build-push-action@v1 + with: + buildx-fallback: false # the fallback is so slow it's better to just fail + push: true + file: production-unit.Dockerfile + tags: ${{ steps.aws-ecr.outputs.registry }}/posthog-cloud:unit + platforms: linux/amd64 + - name: get deployer token id: deployer uses: getsentry/action-github-app-token@v2 diff --git a/bin/docker-server-unit b/bin/docker-server-unit new file mode 100755 index 0000000000000..1eda8374759a5 --- /dev/null +++ b/bin/docker-server-unit @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +# To ensure we are able to expose metrics from multiple processes, we need to +# provide a directory for `prometheus_client` to store a shared registry. +export PROMETHEUS_MULTIPROC_DIR=$(mktemp -d) +chmod -R 777 $PROMETHEUS_MULTIPROC_DIR +trap 'rm -rf "$PROMETHEUS_MULTIPROC_DIR"' EXIT + +export PROMETHEUS_METRICS_EXPORT_PORT=8001 +export STATSD_PORT=${STATSD_PORT:-8125} + +exec /usr/local/bin/docker-entrypoint.sh unitd --no-daemon diff --git a/production-unit.Dockerfile b/production-unit.Dockerfile new file mode 100644 index 0000000000000..59be69641dc21 --- /dev/null +++ b/production-unit.Dockerfile @@ -0,0 +1,210 @@ +# +# This Dockerfile is used for self-hosted production builds. +# +# PostHog has sunset support for self-hosted K8s deployments. +# See: https://posthog.com/blog/sunsetting-helm-support-posthog +# +# Note: for PostHog Cloud remember to update ‘Dockerfile.cloud’ as appropriate. +# +# The stages are used to: +# +# - frontend-build: build the frontend (static assets) +# - plugin-server-build: build plugin-server (Node.js app) & fetch its runtime dependencies +# - posthog-build: fetch PostHog (Django app) dependencies & build Django collectstatic +# - fetch-geoip-db: fetch the GeoIP database +# +# In the last stage, we import the artifacts from the previous +# stages, add some runtime dependencies and build the final image. +# + + +# +# --------------------------------------------------------- +# +FROM node:18.12.1-bullseye-slim AS frontend-build +WORKDIR /code +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +COPY package.json pnpm-lock.yaml ./ +RUN corepack enable && pnpm --version && \ + mkdir /tmp/pnpm-store && \ + pnpm install --frozen-lockfile --store-dir /tmp/pnpm-store --prod && \ + rm -rf /tmp/pnpm-store + +COPY frontend/ frontend/ +COPY ./bin/ ./bin/ +COPY babel.config.js tsconfig.json webpack.config.js ./ +RUN pnpm build + + +# +# --------------------------------------------------------- +# +FROM node:18.12.1-bullseye-slim AS plugin-server-build +WORKDIR /code/plugin-server +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Compile and install Node.js dependencies. +COPY ./plugin-server/package.json ./plugin-server/pnpm-lock.yaml ./plugin-server/tsconfig.json ./ +COPY ./plugin-server/patches/ ./patches/ +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + "make" \ + "g++" \ + "gcc" \ + "python3" \ + "libssl-dev" \ + "zlib1g-dev" \ + && \ + rm -rf /var/lib/apt/lists/* && \ + corepack enable && \ + mkdir /tmp/pnpm-store && \ + pnpm install --frozen-lockfile --store-dir /tmp/pnpm-store && \ + rm -rf /tmp/pnpm-store + +# Build the plugin server. +# +# Note: we run the build as a separate action to increase +# the cache hit ratio of the layers above. +COPY ./plugin-server/src/ ./src/ +RUN pnpm build + +# As the plugin-server is now built, let’s keep +# only prod dependencies in the node_module folder +# as we will copy it to the last image. +RUN corepack enable && \ + mkdir /tmp/pnpm-store && \ + pnpm install --frozen-lockfile --store-dir /tmp/pnpm-store --prod && \ + rm -rf /tmp/pnpm-store + + +# +# --------------------------------------------------------- +# +FROM python:3.10.10-slim-bullseye AS posthog-build +WORKDIR /code +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Compile and install Python dependencies. +# We install those dependencies on a custom folder that we will +# then copy to the last image. +COPY requirements.txt ./ +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + "build-essential" \ + "git" \ + "libpq-dev" \ + "libxmlsec1" \ + "libxmlsec1-dev" \ + "libffi-dev" \ + "pkg-config" \ + && \ + rm -rf /var/lib/apt/lists/* && \ + pip install -r requirements.txt --compile --no-cache-dir --target=/python-runtime + +ENV PATH=/python-runtime/bin:$PATH \ + PYTHONPATH=/python-runtime + +# Add in Django deps and generate Django's static files. +COPY manage.py manage.py +COPY posthog posthog/ +COPY ee ee/ +COPY --from=frontend-build /code/frontend/dist /code/frontend/dist +RUN SKIP_SERVICE_VERSION_REQUIREMENTS=1 SECRET_KEY='unsafe secret key for collectstatic only' DATABASE_URL='postgres:///' REDIS_URL='redis:///' python manage.py collectstatic --noinput + + +# +# --------------------------------------------------------- +# +FROM debian:bullseye-slim AS fetch-geoip-db +WORKDIR /code +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Fetch the GeoLite2-City database that will be used for IP geolocation within Django. +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + "ca-certificates" \ + "curl" \ + "brotli" \ + && \ + rm -rf /var/lib/apt/lists/* && \ + mkdir share && \ + ( curl -s -L "https://mmdbcdn.posthog.net/" | brotli --decompress --output=./share/GeoLite2-City.mmdb ) && \ + chmod -R 755 ./share/GeoLite2-City.mmdb + + +# +# --------------------------------------------------------- +# +FROM nginx/unit:1.28.0-python3.10 +WORKDIR /code +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +ENV PYTHONUNBUFFERED 1 + +# Install OS runtime dependencies. +# Note: please add in this stage runtime dependences only! +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + "chromium" \ + "chromium-driver" \ + "libpq-dev" \ + "libxmlsec1" \ + "libxmlsec1-dev" \ + "libxml2" + +# Install NodeJS 18. +RUN apt-get install -y --no-install-recommends \ + "curl" \ + && \ + curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \ + apt-get install -y --no-install-recommends \ + "nodejs" \ + && \ + rm -rf /var/lib/apt/lists/* + +# Install and use a non-root user. +RUN groupadd -g 1000 posthog && \ + useradd -u 999 -r -g posthog posthog && \ + chown posthog:posthog /code +USER posthog + +# Add in the compiled plugin-server & its runtime dependencies from the plugin-server-build stage. +COPY --from=plugin-server-build --chown=posthog:posthog /code/plugin-server/dist /code/plugin-server/dist +COPY --from=plugin-server-build --chown=posthog:posthog /code/plugin-server/node_modules /code/plugin-server/node_modules +COPY --from=plugin-server-build --chown=posthog:posthog /code/plugin-server/package.json /code/plugin-server/package.json + +# Copy the Python dependencies and Django staticfiles from the posthog-build stage. +COPY --from=posthog-build --chown=posthog:posthog /code/staticfiles /code/staticfiles +COPY --from=posthog-build --chown=posthog:posthog /python-runtime /python-runtime +ENV PATH=/python-runtime/bin:$PATH \ + PYTHONPATH=/python-runtime + +# Copy the frontend assets from the frontend-build stage. +# TODO: this copy should not be necessary, we should remove it once we verify everything still works. +COPY --from=frontend-build --chown=posthog:posthog /code/frontend/dist /code/frontend/dist + +# Copy the GeoLite2-City database from the fetch-geoip-db stage. +COPY --from=fetch-geoip-db --chown=posthog:posthog /code/share/GeoLite2-City.mmdb /code/share/GeoLite2-City.mmdb + +# Add in the Gunicorn config, custom bin files and Django deps. +COPY --chown=posthog:posthog gunicorn.config.py ./ +COPY --chown=posthog:posthog ./bin ./bin/ +COPY --chown=posthog:posthog manage.py manage.py +COPY --chown=posthog:posthog posthog posthog/ +COPY --chown=posthog:posthog ee ee/ +COPY --chown=posthog:posthog hogvm hogvm/ + +# Setup ENV. +ENV NODE_ENV=production \ + CHROME_BIN=/usr/bin/chromium \ + CHROME_PATH=/usr/lib/chromium/ \ + CHROMEDRIVER_BIN=/usr/bin/chromedriver + +# Expose container port and run entry point script. +EXPOSE 8000 + +# Expose the port from which we serve OpenMetrics data. +EXPOSE 8001 +COPY unit.json /docker-entrypoint.d/unit.json +USER root +CMD ["./bin/docker"] diff --git a/unit.json b/unit.json new file mode 100644 index 0000000000000..34f66bff1ba8a --- /dev/null +++ b/unit.json @@ -0,0 +1,16 @@ +{ + "listeners": { + "*:8000": { + "pass": "applications/posthog" + } + }, + "applications": { + "posthog": { + "type": "python 3.10", + "processes": 1, + "working_directory": "/code", + "path": ".", + "module": "posthog.wsgi" + } + } +}