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.
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.
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.
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
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":""},"chroma":{"liveDeploymentHistoryId":"6a20c5961b7c4331fd73ab9e","collectionId":"e7dc639e-c3f6-4a6e-8271-9f1ed255b57c","schemaVersion":"v2"},"entitlements":{},"buildId":"6a20c5961b7c4331fd73ab9e:success","clientVersion":"0.0.3964","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":{},"hasChatPermissions":false,"hasWebMcpPermissions":false,"toggles":[{"name":"kill-isr","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":"editor-v7-file-tree","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":"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":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"pause-universal-search-reconcile","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":"per-site-search-delta","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":"editor-private-page-search","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":"editor-nav-generations","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":"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":"byok-sep-25","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":"vite-origin","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-agent-model-picker","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-updated-9-15","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}],"isAtlas":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:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`git`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` clone`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` https://github.com/open-politics/open-politics-hq.git`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#DCDCAA`},children:`cd`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},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:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`cp`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` .env.example`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` .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:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`docker`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` compose`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 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:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`git`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` clone`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` https://github.com/open-politics/open-politics-hq`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#DCDCAA`},children:`cd`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` .deployments/end-to-end-hetzner-k3s-terraform-helm/open-politics-hq-deployment/hq-cluster-chart`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`cp`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` values.example.yaml`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 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:`#0550AE`,\"--shiki-dark\":`#DCDCAA`},children:`cd`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` .deployments/end-to-end-hetzner-k3s-terraform-helm/open-politics-hq-deployment/`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`cp`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` .tfvars.example`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` .tfvars`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`cp`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` hq-cluster-chart/values.example.yaml`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 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:`#953800`,\"--shiki-dark\":`#DCDCAA`},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:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`cp`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` .tfvars.example`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` .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:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`cp`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` hq-cluster-chart/values.example.yaml`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 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:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`./scripts/manage-ssh-keys.sh`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 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:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`kubectl`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` get`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` svc`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` traefik`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -n`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` kube-system`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -o`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` jsonpath='{.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:`#953800`,\"--shiki-dark\":`#DCDCAA`},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:`#953800`,\"--shiki-dark\":`#DCDCAA`},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:`#953800`,\"--shiki-dark\":`#DCDCAA`},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.app/_mintlify/api/og/v2?division=Getting+Started\u0026amp;title=Installation+%26+Architecture\u0026amp;description=Self-hosting+guide+and+platform+architecture\u0026amp;theme=b001022abe6a9c7b6bafd330\u0026amp;signature=7c2c078ee358eefe4de16f5ffbd91f7f397131396f2eacf92f859b06c802450e\">\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.app/_mintlify/api/og/v2?division=Getting+Started\u0026amp;title=Installation+%26+Architecture\u0026amp;description=Self-hosting+guide+and+platform+architecture\u0026amp;theme=b001022abe6a9c7b6bafd330\u0026amp;signature=7c2c078ee358eefe4de16f5ffbd91f7f397131396f2eacf92f859b06c802450e\">\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\",\"creator\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","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"}]}]},"title":"Installation \u0026 Architecture - Open Politics Documentation","buildId":"dpl_HcHmUS6i3UwswnTHJUEdcTtwfdTQ","deploymentId":"dpl_HcHmUS6i3UwswnTHJUEdcTtwfdTQ","dynamics":[],"chunks":[]}