Begin using the platform with pre-loaded data and continue to:
App Setup
Configure your API keys and providers
Our preferred way to self-host on a single machine or develop with is Docker Compose. For Multi-machine setups, we recommend Kubernetes.But in both the cases you can choose to outsource services to managed providers for a more lightweight (but less private) setup.
cp .env.example .env# Edit .env with your configuration# All values with changeThis need to be changed before the app will start.# for example with 'openssl rand -base64 14' in your terminal.
4
Start services
docker compose up
5
Access the platform
Open your browser and navigate to:
http://localhost:3000
Initial setup may take time depending on your system configuration.
If you are here, we assume you know what you are doing.
git clone https://github.com/open-politics/open-politics-hqcd .deployments/end-to-end-hetzner-k3s-terraform-helm/open-politics-hq-deployment/hq-cluster-chartcp values.example.yaml values.yaml# Edit values.yaml with your configuration
Hetzner Kubernetes Stack
Our easiest way to deploy HQ via Kubernetes is to use Hetzner. We provide an end-to-end script that provisions a master and worker nodes via Terraform and installs K3s. We suggest using K9s alongside our scripts to manage and scale the cluster.
Hetzner Kubernetes Stack
HQ on Hetzner (K3s) — End-to-End Deployment Guide
This repository delivers a batteries-included, reproducible way to provision a Hetzner K3s cluster and deploy your application stack with one command.
What This Gives You
3 Hetzner Cloud servers by default: 1 control plane, 2 workers (configurable)
K3s with sensible flags for Hetzner CCM integration
Traefik Ingress with automatic TLS via Let’s Encrypt (ACME)
cd .deployments/end-to-end-hetzner-k3s-terraform-helm/open-politics-hq-deployment/cp .tfvars.example .tfvarscp hq-cluster-chart/values.example.yaml hq-cluster-chart/values.yaml# Edit .tfvars and values.yaml with your configuration (see below)
2
Deploy
./deploy.sh
The script is idempotent. Re-running applies updates safely. Typical first deploy time: ~10–15 minutes.
Configuration Checklist
Before running ./deploy.sh, ensure you’ve completed:
Hetzner API Token: Set hcloud_token in .tfvars
Domain: Set domain in .tfvars (must match env.config.DOMAIN in values.yaml)
Application Values: Copy and customize hq-cluster-chart/values.yaml
Secrets: Replace all xxx placeholders with real values
SSH Keys: Run ./scripts/manage-ssh-keys.sh generate
DNS Ready: Have your domain ready for A record creation
Prerequisites
Hetzner Cloud account and API token (Read/Write)
Local tools: terraform, kubectl, helm, jq
An SSH key uploaded to Hetzner; the scripts can rotate/manage keys automatically
API keys: OPENAI_API_KEY, TAVILY_API_KEY, GOOGLE_API_KEY, MAPBOX_ACCESS_TOKEN
3
SSH Key Management
./scripts/manage-ssh-keys.sh generate
This generates key pairs, uploads to Hetzner, and updates cloud-init templates.Manual alternative: Upload your SSH key to Hetzner Console and set ssh_key_name in .tfvars.
4
DNS Preparation
Have your domain ready. After deployment, create A records pointing to the Load Balancer IP for both your-domain.com and www.your-domain.com.
kubectl get svc traefik -n kube-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
2
Create DNS A records
Point your domain (and www) to the Load Balancer IP. Traefik will obtain Let’s Encrypt certificates automatically (typically 2-5 minutes after DNS propagation).
3
Verify deployment
./scripts/test-deployment.sh # Test DNS, HTTP/HTTPS, and TLS./scripts/status.sh # View pods, services, ingress./scripts/check-ssl.sh # Check ACME progress
Troubleshooting
Issue
Solution
Workers don’t schedule pods
CCM removes taints automatically; wait or remove manually
No Load Balancer IP
Confirm Traefik is LoadBalancer and CCM is Ready
Domain mismatch
Ensure .tfvars domain matches values.yaml
CORS errors
Verify BACKEND_CORS_ORIGINS includes your domain
ACME stuck
Verify DNS A records point to LB IP; run ./scripts/check-ssl.sh
SSH failures
Run ./scripts/manage-ssh-keys.sh generate
Cost
Default: 3× cx22 + 1 Load Balancer ≈ €40–45/month. Reduce costs by lowering worker_count or using smaller server types.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"Open Politics Documentation","colors":{"primary":"#0D9373","light":"#07C983","dark":"#0D9373"},"logo":{"light":"https://mintcdn.com/openpolitics/QP-S-vUguhbR_Sxa/logo/opp_white.svg?fit=max\u0026auto=format\u0026n=QP-S-vUguhbR_Sxa\u0026q=85\u0026s=25ab251a1c6404fed5bf21e01658e1ae","dark":"https://mintcdn.com/openpolitics/QP-S-vUguhbR_Sxa/logo/opp_black.svg?fit=max\u0026auto=format\u0026n=QP-S-vUguhbR_Sxa\u0026q=85\u0026s=d4b2e0d70793405bbfe87da3ea75d4b7","href":"https://open-politics.org"},"favicon":"/favicon.ico","api":{},"navbar":{"primary":{"type":"button","label":"HQ Webapp","href":"https://open-politics.org/hq"}},"footer":{"socials":{"github":"https://github.com/open-politics","linkedin":"https://www.linkedin.com/company/102941572"}},"search":{},"styling":{"latex":false},"navigation":{"global":{"anchors":[{"anchor":"Forum","icon":"comments","href":"https://forum.open-politics.org"},{"anchor":"HQ Webapp","icon":"arrow-up-right-from-square","href":"https://open-politics.org/hq"}]},"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"openpolitics","hasAudioNarration":false,"actualSubdomain":"openpolitics","customDomains":["docs.open-politics.org"],"gitSource":{"type":"github","owner":"jimvincentw","repo":"docs","deployBranch":"main","contentDirectory":"","isPrivate":false,"mountPath":"","label":""},"entitlements":{},"buildId":"6ae921bffe09c0a3f9d2ebb4:success","clientVersion":"0.0.4131","searchFilterCounts":[{"language":"default","tag":"Core Concepts","count":5},{"language":"default","tag":"Getting Started","count":3},{"language":"default","tag":"How To","count":2},{"language":"default","tag":"Project","count":1}]},"assistantConfig":{},"basePath":"","hasChatPermissions":false,"hasWebMcpPermissions":false,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-rbac-roles","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"rbac-expansion","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"full-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-sso-scim","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-signup","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-dual-write","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-enabled","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"approve-review-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-metadata","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"sso-setup-deadline-banner","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-serve","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"live-agents","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"googleFontsInlined":false},"customCss":[{"filePath":"style.css","content":".nav-logo{object-fit:scale-down}"}],"page":{"pagePath":"pages/app/installation-self-hosted","slug":"/pages/app/installation-self-hosted","pageMetadata":{"title":"Installation \u0026 Architecture","description":"Self-hosting guide and platform architecture","href":"/pages/app/installation-self-hosted"},"description":{"compiledSource":"\"use strict\";\nconst {jsx: _jsx} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n p: \"p\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsx(_components.p, {\n children: \"Self-hosting guide and platform architecture\"\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx,jsxs:_jsxs}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){const _components={a:`a`,code:`code`,hr:`hr`,input:`input`,li:`li`,ol:`ol`,p:`p`,pre:`pre`,span:`span`,strong:`strong`,tbody:`tbody`,td:`td`,th:`th`,thead:`thead`,tr:`tr`,ul:`ul`,..._provideComponents(),...props.components},{Accordion,Card,CodeBlock,Heading,Note,Step,Steps,Tab,Table,Tabs}=_components;if(!Accordion)_missingMdxReference(`Accordion`,true);if(!Card)_missingMdxReference(`Card`,true);if(!CodeBlock)_missingMdxReference(`CodeBlock`,true);if(!Heading)_missingMdxReference(`Heading`,true);if(!Note)_missingMdxReference(`Note`,true);if(!Step)_missingMdxReference(`Step`,true);if(!Steps)_missingMdxReference(`Steps`,true);if(!Tab)_missingMdxReference(`Tab`,true);if(!Table)_missingMdxReference(`Table`,true);if(!Tabs)_missingMdxReference(`Tabs`,true);return _jsxs(_Fragment,{children:[_jsx(Heading,{level:`2`,id:`how-to-set-up-your-hq`,children:`How to set up your HQ`}),`\n`,_jsxs(Tabs,{children:[_jsxs(Tab,{title:`Hosted`,id:`hosted`,children:[_jsx(_components.p,{children:`The simplest way to use Open Politics is through our hosted platform:`}),_jsxs(_components.ol,{children:[`\n`,_jsxs(_components.li,{children:[`Register at `,_jsx(_components.a,{href:`https://open-politics.org/accounts/register`,children:`open-politics.org`})]}),`\n`,_jsxs(_components.li,{children:[`Log in at `,_jsx(_components.a,{href:`https://open-politics.org/accounts/login`,children:`open-politics.org/`})]}),`\n`,_jsx(_components.li,{children:`Begin using the platform with pre-loaded data and continue to:`}),`\n`]}),_jsx(Card,{icon:`cog`,href:`/pages/app/app-setup`,title:`App Setup`,children:_jsx(_components.p,{children:`Configure your API keys and providers`})})]}),_jsxs(Tab,{title:`Self-hosted`,id:`self-hosted`,\"data-child-tab-ids\":`[\"docker-compose\",\"kubernetes\"]`,\"data-child-heading-ids\":`[\"base-helm-chart\",\"hetzner-kubernetes-stack\",\"hq-on-hetzner-k3s-%E2%80%94-end-to-end-deployment-guide\",\"what-this-gives-you\",\"quick-start\",\"configuration-checklist\",\"prerequisites\",\"initial-configuration\",\"how-it-works\",\"dns-and-tls\",\"troubleshooting\",\"cost\"]`,children:[_jsx(_components.p,{children:`Our preferred way to self-host on a single machine or develop with is Docker Compose. For Multi-machine setups, we recommend Kubernetes.`}),_jsx(_components.p,{children:`But in both the cases you can choose to outsource services to managed providers for a more lightweight (but less private) setup.`}),_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Service`}),_jsx(_components.th,{children:`Purpose`}),_jsx(_components.th,{children:`Example Provider`}),_jsx(_components.th,{})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Postgres`}),_jsx(_components.td,{children:`Main database`}),_jsx(_components.td,{children:`Hetzner`}),_jsx(_components.td,{})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Redis`}),_jsx(_components.td,{children:`Cache and queue`}),_jsx(_components.td,{children:`Upstash`}),_jsx(_components.td,{})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`MinIO / S3`}),_jsx(_components.td,{children:`Object storage`}),_jsx(_components.td,{children:`Hetzner`}),_jsx(_components.td,{})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Nominatim`}),_jsx(_components.td,{children:`Geocoding`}),_jsx(_components.td,{children:`Nominatim`}),_jsx(_components.td,{children:`- uses lot of disk space (170gb+)`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`LLM API`}),_jsx(_components.td,{children:`Language model`}),_jsx(_components.td,{children:`OpenAI`}),_jsx(_components.td,{children:`- high gpu and/or cpu usage`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Tavily / SerpAPI`}),_jsx(_components.td,{children:`Web search`}),_jsx(_components.td,{children:`Tavily`}),_jsx(_components.td,{})]})]})]}),_jsxs(Tabs,{children:[_jsxs(Tab,{title:`Docker Compose`,id:`docker-compose`,children:[_jsx(_components.p,{children:`Run the complete platform locally:`}),_jsxs(Steps,{children:[_jsx(Step,{title:`Prerequisites`,children:_jsxs(_components.ul,{children:[`\n`,_jsx(_components.li,{children:`Docker and Docker Compose`}),`\n`,_jsx(_components.li,{children:`32GB RAM recommended`}),`\n`,_jsx(_components.li,{children:`20GB free disk space`}),`\n`,_jsx(_components.li,{children:`Git`}),`\n`]})}),_jsx(Step,{title:`Clone repository`,children:_jsx(CodeBlock,{filename:``,numberOfLines:`2`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`2`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`git`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` clone https://github.com/open-politics/open-politics-hq.git`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`cd`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` open-politics-hq`})]}),`\n`]})})})}),_jsx(Step,{title:`Configure environment`,children:_jsx(CodeBlock,{filename:``,numberOfLines:`4`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`4`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`cp .env.example .env`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# Edit .env with your configuration`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# All values with changeThis need to be changed before the app will start.`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# for example with 'openssl rand -base64 14' in your terminal.`})}),`\n`]})})})}),_jsx(Step,{title:`Start services`,children:_jsx(CodeBlock,{filename:``,numberOfLines:`1`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`1`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`docker`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` compose up`})]}),`\n`]})})})}),_jsxs(Step,{title:`Access the platform`,children:[_jsx(_components.p,{children:`Open your browser and navigate to:`}),_jsx(CodeBlock,{numberOfLines:`2`,language:`text`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`text`,children:_jsxs(_components.code,{language:`text`,numberOfLines:`2`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`http://localhost:3000`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{})})]})})})]})]}),_jsx(Note,{children:_jsx(_components.p,{children:`Initial setup may take time depending on your system configuration.`})})]}),_jsxs(Tab,{title:`Kubernetes`,id:`kubernetes`,\"data-child-heading-ids\":`[\"base-helm-chart\",\"hetzner-kubernetes-stack\",\"hq-on-hetzner-k3s-%E2%80%94-end-to-end-deployment-guide\",\"what-this-gives-you\",\"quick-start\",\"configuration-checklist\",\"prerequisites\",\"initial-configuration\",\"how-it-works\",\"dns-and-tls\",\"troubleshooting\",\"cost\"]`,children:[_jsx(Note,{children:`If you are here, we assume you know what you are doing.`}),_jsx(Heading,{level:`2`,id:`base-helm-chart`,children:`Base Helm Chart`}),_jsxs(_components.p,{children:[`We have a base helm chart available `,_jsx(_components.a,{href:`https://github.com/open-politics/open-politics-hq/tree/main/.deployments`,children:`here`})]}),_jsx(CodeBlock,{filename:``,numberOfLines:`4`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`4`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`git`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` clone https://github.com/open-politics/open-politics-hq`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`cd`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` .deployments/end-to-end-hetzner-k3s-terraform-helm/open-politics-hq-deployment/hq-cluster-chart`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`cp values.example.yaml values.yaml`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# Edit values.yaml with your configuration`})}),`\n`]})})}),_jsx(Heading,{level:`2`,id:`hetzner-kubernetes-stack`,children:`Hetzner Kubernetes Stack`}),_jsx(_components.p,{children:`Our easiest way to deploy HQ via Kubernetes is to use Hetzner. We provide an end-to-end script that provisions a master and worker nodes via Terraform and installs K3s. We suggest using K9s alongside our scripts to manage and scale the cluster.`}),_jsxs(Accordion,{title:`Hetzner Kubernetes Stack`,id:`hetzner-kubernetes-stack`,children:[_jsx(Heading,{level:`1`,id:`hq-on-hetzner-k3s-%E2%80%94-end-to-end-deployment-guide`,children:`HQ on Hetzner (K3s) — End-to-End Deployment Guide`}),_jsx(_components.p,{children:`This repository delivers a batteries-included, reproducible way to provision a Hetzner K3s cluster and deploy your application stack with one command.`}),_jsx(Heading,{level:`2`,id:`what-this-gives-you`,children:`What This Gives You`}),_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`3 Hetzner Cloud servers by default`}),`: 1 control plane, 2 workers (configurable)`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`K3s`}),` with sensible flags for Hetzner CCM integration`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Traefik`}),` Ingress with automatic TLS via Let’s Encrypt (ACME)`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Persistent storage`}),` using Hetzner CSI`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Application stack via Helm`}),`: frontend, backend API, Celery workers, Redis, optional Postgres/MinIO templates`]}),`\n`]}),_jsx(_components.hr,{}),_jsx(Heading,{level:`2`,id:`quick-start`,children:`Quick Start`}),_jsxs(Steps,{children:[_jsx(Step,{title:`Configure Hetzner token`,children:_jsx(CodeBlock,{filename:``,numberOfLines:`4`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`4`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`cd`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` .deployments/end-to-end-hetzner-k3s-terraform-helm/open-politics-hq-deployment/`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`cp .tfvars.example .tfvars`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`cp hq-cluster-chart/values.example.yaml hq-cluster-chart/values.yaml`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# Edit .tfvars and values.yaml with your configuration (see below)`})}),`\n`]})})})}),_jsxs(Step,{title:`Deploy`,children:[_jsx(CodeBlock,{filename:``,numberOfLines:`1`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`1`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`./deploy.sh`})}),`\n`]})})}),_jsx(_components.p,{children:`The script is idempotent. Re-running applies updates safely. Typical first deploy time: ~10–15 minutes.`})]})]}),_jsx(Heading,{level:`2`,id:`configuration-checklist`,children:`Configuration Checklist`}),_jsxs(_components.p,{children:[`Before running `,_jsx(_components.code,{children:`./deploy.sh`}),`, ensure you’ve completed:`]}),_jsxs(_components.ul,{className:`contains-task-list`,children:[`\n`,_jsxs(_components.li,{className:`task-list-item`,children:[_jsx(_components.input,{type:`checkbox`,disabled:true}),` `,_jsx(_components.strong,{children:`Hetzner API Token`}),`: Set `,_jsx(_components.code,{children:`hcloud_token`}),` in `,_jsx(_components.code,{children:`.tfvars`})]}),`\n`,_jsxs(_components.li,{className:`task-list-item`,children:[_jsx(_components.input,{type:`checkbox`,disabled:true}),` `,_jsx(_components.strong,{children:`Domain`}),`: Set `,_jsx(_components.code,{children:`domain`}),` in `,_jsx(_components.code,{children:`.tfvars`}),` (must match `,_jsx(_components.code,{children:`env.config.DOMAIN`}),` in `,_jsx(_components.code,{children:`values.yaml`}),`)`]}),`\n`,_jsxs(_components.li,{className:`task-list-item`,children:[_jsx(_components.input,{type:`checkbox`,disabled:true}),` `,_jsx(_components.strong,{children:`Application Values`}),`: Copy and customize `,_jsx(_components.code,{children:`hq-cluster-chart/values.yaml`})]}),`\n`,_jsxs(_components.li,{className:`task-list-item`,children:[_jsx(_components.input,{type:`checkbox`,disabled:true}),` `,_jsx(_components.strong,{children:`Secrets`}),`: Replace all `,_jsx(_components.code,{children:`xxx`}),` placeholders with real values`]}),`\n`,_jsxs(_components.li,{className:`task-list-item`,children:[_jsx(_components.input,{type:`checkbox`,disabled:true}),` `,_jsx(_components.strong,{children:`SSH Keys`}),`: Run `,_jsx(_components.code,{children:`./scripts/manage-ssh-keys.sh generate`})]}),`\n`,_jsxs(_components.li,{className:`task-list-item`,children:[_jsx(_components.input,{type:`checkbox`,disabled:true}),` `,_jsx(_components.strong,{children:`DNS Ready`}),`: Have your domain ready for A record creation`]}),`\n`]}),_jsx(_components.hr,{}),_jsx(Heading,{level:`2`,id:`prerequisites`,children:`Prerequisites`}),_jsxs(_components.ul,{children:[`\n`,_jsx(_components.li,{children:`Hetzner Cloud account and API token (Read/Write)`}),`\n`,_jsxs(_components.li,{children:[`Local tools: `,_jsx(_components.code,{children:`terraform`}),`, `,_jsx(_components.code,{children:`kubectl`}),`, `,_jsx(_components.code,{children:`helm`}),`, `,_jsx(_components.code,{children:`jq`})]}),`\n`,_jsx(_components.li,{children:`An SSH key uploaded to Hetzner; the scripts can rotate/manage keys automatically`}),`\n`]}),_jsx(_components.hr,{}),_jsx(Heading,{level:`2`,id:`initial-configuration`,children:`Initial Configuration`}),_jsxs(Steps,{children:[_jsxs(Step,{title:`Infrastructure Configuration (.tfvars)`,children:[_jsx(CodeBlock,{filename:``,numberOfLines:`1`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`1`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`cp .tfvars.example .tfvars`})}),`\n`]})})}),_jsx(_components.p,{children:_jsx(_components.strong,{children:`Required:`})}),_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`hcloud_token`}),`: Hetzner API token (`,_jsx(_components.a,{href:`https://console.hetzner.com/projects`,children:`get from console`}),`)`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`domain`}),`: your public domain for TLS/ingress`]}),`\n`]}),_jsx(_components.p,{children:_jsx(_components.strong,{children:`Optional (with defaults):`})}),_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`worker_count`}),`: number of workers (default: 2)`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`master_server_type`}),` / `,_jsx(_components.code,{children:`worker_server_type`}),`: server types (default: `,_jsx(_components.code,{children:`cx22`}),`)`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`location`}),`: Hetzner location (default: `,_jsx(_components.code,{children:`fsn1`}),`)`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`cluster_slug`}),`: resource naming prefix (default: `,_jsx(_components.code,{children:`hq`}),`)`]}),`\n`]})]}),_jsxs(Step,{title:`Application Configuration (values.yaml)`,children:[_jsx(CodeBlock,{filename:``,numberOfLines:`1`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`1`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`cp hq-cluster-chart/values.example.yaml hq-cluster-chart/values.yaml`})}),`\n`]})})}),_jsx(_components.p,{children:_jsx(_components.strong,{children:`Required customizations:`})}),_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`email`}),`: admin email for Let’s Encrypt`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`env.config.DOMAIN`}),`: your domain (must match `,_jsx(_components.code,{children:`.tfvars`}),`)`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`env.config.BACKEND_CORS_ORIGINS`}),`: update with your domain`]}),`\n`,_jsx(_components.li,{children:`Docker image repositories for backend, frontend, and celery-worker`}),`\n`]}),_jsx(_components.p,{children:_jsxs(_components.strong,{children:[`Secrets (replace all `,_jsx(_components.code,{children:`xxx`}),` placeholders):`]})}),_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`SECRET_KEY`}),`, `,_jsx(_components.code,{children:`FIRST_SUPERUSER_PASSWORD`}),`, `,_jsx(_components.code,{children:`POSTGRES_PASSWORD`})]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`MINIO_ROOT_PASSWORD`}),`, `,_jsx(_components.code,{children:`MINIO_SECRET_KEY`})]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`SMTP_USER`}),`, `,_jsx(_components.code,{children:`SMTP_PASSWORD`}),`, `,_jsx(_components.code,{children:`EMAILS_FROM_EMAIL`})]}),`\n`,_jsxs(_components.li,{children:[`API keys: `,_jsx(_components.code,{children:`OPENAI_API_KEY`}),`, `,_jsx(_components.code,{children:`TAVILY_API_KEY`}),`, `,_jsx(_components.code,{children:`GOOGLE_API_KEY`}),`, `,_jsx(_components.code,{children:`MAPBOX_ACCESS_TOKEN`})]}),`\n`]})]}),_jsxs(Step,{title:`SSH Key Management`,children:[_jsx(CodeBlock,{filename:``,numberOfLines:`1`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`1`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`./scripts/manage-ssh-keys.sh generate`})}),`\n`]})})}),_jsx(_components.p,{children:`This generates key pairs, uploads to Hetzner, and updates cloud-init templates.`}),_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`Manual alternative:`}),` Upload your SSH key to Hetzner Console and set `,_jsx(_components.code,{children:`ssh_key_name`}),` in `,_jsx(_components.code,{children:`.tfvars`}),`.`]})]}),_jsx(Step,{title:`DNS Preparation`,children:_jsxs(_components.p,{children:[`Have your domain ready. After deployment, create A records pointing to the Load Balancer IP for both `,_jsx(_components.code,{children:`your-domain.com`}),` and `,_jsx(_components.code,{children:`www.your-domain.com`}),`.`]})})]}),_jsx(_components.hr,{}),_jsx(Heading,{level:`2`,id:`how-it-works`,children:`How It Works`}),_jsxs(_components.p,{children:[`The `,_jsx(_components.code,{children:`./deploy.sh`}),` orchestrator performs two phases:`]}),_jsxs(_components.ol,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Provision infrastructure`}),`: Terraform creates network, firewall, servers; cloud-init installs K3s`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Deploy stack`}),`: Configures Traefik, installs Hetzner CCM/CSI, deploys the Helm chart`]}),`\n`]}),_jsx(_components.p,{children:_jsx(_components.strong,{children:`Core commands:`})}),_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Command`}),_jsx(_components.th,{children:`Description`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`./deploy.sh`})}),_jsx(_components.td,{children:`Full deploy (infra → app)`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`./scripts/connect.sh`})}),_jsx(_components.td,{children:`Fetch kubeconfig and set kubectl context`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`./scripts/status.sh`})}),_jsx(_components.td,{children:`Summary of nodes, services, ingress, PVs`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`./scripts/logs.sh`})}),_jsx(_components.td,{children:`Tail app logs`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`./scripts/monitor.sh`})}),_jsx(_components.td,{children:`Open k9s`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`./scripts/destroy.sh`})}),_jsx(_components.td,{children:`Tear everything down`})]})]})]}),_jsx(_components.hr,{}),_jsx(Heading,{level:`2`,id:`dns-and-tls`,children:`DNS and TLS`}),_jsxs(Steps,{children:[_jsx(Step,{title:`Get Load Balancer IP`,children:_jsx(CodeBlock,{filename:``,numberOfLines:`1`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`1`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`kubectl get svc traefik -n kube-system -o jsonpath=`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`'{.status.loadBalancer.ingress[0].ip}'`})]}),`\n`]})})})}),_jsx(Step,{title:`Create DNS A records`,children:_jsxs(_components.p,{children:[`Point your domain (and `,_jsx(_components.code,{children:`www`}),`) to the Load Balancer IP. Traefik will obtain Let’s Encrypt certificates automatically (typically 2-5 minutes after DNS propagation).`]})}),_jsx(Step,{title:`Verify deployment`,children:_jsx(CodeBlock,{filename:``,numberOfLines:`3`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`3`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`./scripts/test-deployment.sh `}),_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# Test DNS, HTTP/HTTPS, and TLS`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`./scripts/status.sh `}),_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# View pods, services, ingress`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`./scripts/check-ssl.sh `}),_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# Check ACME progress`})]}),`\n`]})})})})]}),_jsx(_components.hr,{}),_jsx(Heading,{level:`2`,id:`troubleshooting`,children:`Troubleshooting`}),_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Issue`}),_jsx(_components.th,{children:`Solution`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Workers don’t schedule pods`}),_jsx(_components.td,{children:`CCM removes taints automatically; wait or remove manually`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`No Load Balancer IP`}),_jsxs(_components.td,{children:[`Confirm Traefik is `,_jsx(_components.code,{children:`LoadBalancer`}),` and CCM is Ready`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Domain mismatch`}),_jsxs(_components.td,{children:[`Ensure `,_jsx(_components.code,{children:`.tfvars`}),` domain matches `,_jsx(_components.code,{children:`values.yaml`})]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`CORS errors`}),_jsxs(_components.td,{children:[`Verify `,_jsx(_components.code,{children:`BACKEND_CORS_ORIGINS`}),` includes your domain`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`ACME stuck`}),_jsxs(_components.td,{children:[`Verify DNS A records point to LB IP; run `,_jsx(_components.code,{children:`./scripts/check-ssl.sh`})]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`SSH failures`}),_jsxs(_components.td,{children:[`Run `,_jsx(_components.code,{children:`./scripts/manage-ssh-keys.sh generate`})]})]})]})]}),_jsx(Heading,{level:`2`,id:`cost`,children:`Cost`}),_jsxs(_components.p,{children:[`Default: 3× `,_jsx(_components.code,{children:`cx22`}),` + 1 Load Balancer ≈ €40–45/month. Reduce costs by lowering `,_jsx(_components.code,{children:`worker_count`}),` or using smaller server types.`]})]})]})]})]})]})]})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent};function _missingMdxReference(id,component){throw new Error(`Expected `+(component?`component`:`object`)+\" `\"+id+\"` to be defined: you likely forgot to import, pass, or provide it.\")}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Installation \u0026 Architecture","description":"Self-hosting guide and platform architecture","href":"/pages/app/installation-self-hosted"}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[{"title":"How to set up your HQ","slug":"how-to-set-up-your-hq","depth":2,"children":[]},{"title":"Base Helm Chart","slug":"base-helm-chart","depth":2,"children":[],"tabId":"kubernetes"},{"title":"Hetzner Kubernetes Stack","slug":"hetzner-kubernetes-stack","depth":2,"children":[],"tabId":"kubernetes"}],"codeExamples":{}},"jsLinks":[],"subdomain":"openpolitics","siteName":"Open Politics Documentation","title":"Installation \u0026 Architecture - Open Politics Documentation","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{}},"lastModified":"2025-12-18T14:13:10.297Z","originalFileLocation":"pages/app/installation-self-hosted.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"system\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"Self-hosting guide and platform architecture\">\u003cmeta data-mint-head name=\"application-name\" content=\"Open Politics Documentation\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/openpolitics/2a8p7n2GfakmAQ_k/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/openpolitics/2a8p7n2GfakmAQ_k/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/openpolitics/2a8p7n2GfakmAQ_k/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/openpolitics/2a8p7n2GfakmAQ_k/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/openpolitics/2a8p7n2GfakmAQ_k/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/openpolitics/2a8p7n2GfakmAQ_k/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/openpolitics/2a8p7n2GfakmAQ_k/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/openpolitics/2a8p7n2GfakmAQ_k/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/openpolitics/2a8p7n2GfakmAQ_k/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Installation \u0026amp; Architecture - Open Politics Documentation\">\u003cmeta data-mint-head property=\"og:description\" content=\"Self-hosting guide and platform architecture\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"Open Politics Documentation\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://docs.open-politics.org/pages/app/installation-self-hosted\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://openpolitics.mintlify.site/_mintlify/api/og/v2?division=Getting+Started\u0026amp;title=Installation+%26+Architecture\u0026amp;description=Self-hosting+guide+and+platform+architecture\u0026amp;theme=cdbc5ba54309a4e827ba93a5\u0026amp;signature=31483d8a1c38d5337af996f94c0544e682fc6639592a2728ed802c4da484c11c\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Installation \u0026amp; Architecture - Open Politics Documentation\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"Self-hosting guide and platform architecture\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://openpolitics.mintlify.site/_mintlify/api/og/v2?division=Getting+Started\u0026amp;title=Installation+%26+Architecture\u0026amp;description=Self-hosting+guide+and+platform+architecture\u0026amp;theme=cdbc5ba54309a4e827ba93a5\u0026amp;signature=31483d8a1c38d5337af996f94c0544e682fc6639592a2728ed802c4da484c11c\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/openpolitics/2a8p7n2GfakmAQ_k/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"Open Politics Documentation\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#0D9373\">\u003clink data-mint-head rel=\"canonical\" href=\"https://docs.open-politics.org/pages/app/installation-self-hosted\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/pages/app/installation-self-hosted.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://docs.open-politics.org/#organization\",\"name\":\"Open Politics Documentation\",\"url\":\"https://docs.open-politics.org\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/openpolitics/QP-S-vUguhbR_Sxa/logo/opp_white.svg?fit=max\u0026auto=format\u0026n=QP-S-vUguhbR_Sxa\u0026q=85\u0026s=25ab251a1c6404fed5bf21e01658e1ae\"}},{\"@type\":\"WebSite\",\"@id\":\"https://docs.open-politics.org#website\",\"name\":\"Open Politics Documentation\",\"url\":\"https://docs.open-politics.org\",\"publisher\":{\"@id\":\"https://docs.open-politics.org/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://docs.open-politics.org/pages/app/installation-self-hosted#webpage\",\"url\":\"https://docs.open-politics.org/pages/app/installation-self-hosted\",\"name\":\"Installation \u0026 Architecture\",\"description\":\"Self-hosting guide and platform architecture\",\"dateModified\":\"2025-12-18T14:13:10.297Z\",\"isPartOf\":{\"@id\":\"https://docs.open-politics.org#website\"},\"breadcrumb\":{\"@id\":\"https://docs.open-politics.org/pages/app/installation-self-hosted#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://docs.open-politics.org/pages/app/installation-self-hosted#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Getting Started\",\"item\":\"https://docs.open-politics.org/pages/app/overview\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Installation \u0026 Architecture\",\"item\":\"https://docs.open-politics.org/pages/app/installation-self-hosted\"}]},{\"@type\":[\"Article\",\"TechArticle\"],\"@id\":\"https://docs.open-politics.org/pages/app/installation-self-hosted#article\",\"headline\":\"Installation \u0026 Architecture\",\"name\":\"Installation \u0026 Architecture\",\"description\":\"Self-hosting guide and platform architecture\",\"url\":\"https://docs.open-politics.org/pages/app/installation-self-hosted\",\"mainEntityOfPage\":{\"@id\":\"https://docs.open-politics.org/pages/app/installation-self-hosted#webpage\"},\"dateModified\":\"2025-12-18T14:13:10.297Z\",\"publisher\":{\"@id\":\"https://docs.open-politics.org/#organization\"},\"isPartOf\":{\"@id\":\"https://docs.open-politics.org#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"Open Politics Documentation\",\"publisher\":{\"@type\":\"Organization\",\"name\":\"Open Politics Documentation\"},\"provider\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","agentPointerHref":"/pages/app/installation-self-hosted.md","siteName":"Open Politics Documentation","nav":{"global":{"anchors":[{"anchor":"Forum","icon":"comments","href":"https://forum.open-politics.org"},{"anchor":"HQ Webapp","icon":"arrow-up-right-from-square","href":"https://open-politics.org/hq"}]},"groups":[{"group":"Project","pages":[{"title":"Manifest","href":"/pages/project/manifest"}]},{"group":"Getting Started","pages":[{"title":"Overview","description":"How HQ is organised and where to find what","href":"/pages/app/overview"},{"title":"Installation \u0026 Architecture","href":"/pages/app/installation-self-hosted"},{"title":"App Setup","description":"Configure providers and foundation services","href":"/pages/app/app-setup"}]},{"group":"Core Concepts","pages":[{"title":"Infospaces","href":"/pages/app/infospaces"},{"title":"Assets \u0026 Bundles","href":"/pages/app/assets"},{"title":"Schemas","href":"/pages/app/schemas"},{"title":"Dashboards","href":"/pages/app/dashboards"},{"title":"Chat","href":"/pages/app/chat"}]},{"group":"How To","pages":[{"title":"Running Analysis","href":"/pages/concepts/running-analysis"},{"title":"Curating Annotations","href":"/pages/concepts/curating-fragments"}]}]},"basePath":"","title":"Installation \u0026 Architecture - Open Politics Documentation","buildId":"dpl_5joLUXmnJgwqjrHAS4dxZrQTdxgh","deploymentId":"dpl_5joLUXmnJgwqjrHAS4dxZrQTdxgh","dynamics":[],"chunks":[]}