
Securing Your European Cloud Footprint: IAM and Network Policy Best Practices
June 5, 2026
Managed Kubernetes on OVHcloud vs Scaleway: a first look for web app teams
June 29, 2026Why European Kubernetes?
For engineering teams building web applications that serve European users, the choice of infrastructure provider carries more weight than it used to. Two forces are driving this: regulatory pressure and a maturing European cloud ecosystem that no longer requires meaningful trade-offs to stay on-continent.
Data sovereignty is the clearest reason to go European. GDPR requires that personal data of EU residents be processed under conditions that guarantee adequate protection. Hosting on OVHcloud, Scaleway, or Hetzner means your data never leaves European jurisdiction; no Standard Contractual Clauses, no reliance on a hyperscaler whose parent company is subject to foreign surveillance law.
The capability gap has closed. Managed Kubernetes, container registries, load balancers, block storage, object storage; all three major European providers offer these now. The open-source tooling (Helm, cert-manager, Prometheus) is provider-agnostic throughout, which means your pipeline is fully portable if you ever need to move.
Choosing a European Provider
OVHcloud Managed Kubernetes Service
The most enterprise-ready option. OVHcloud MKS gives you a fully managed control plane, automatic node upgrades, and native integration with OVHcloud Load Balancers. Best choice for compliance-heavy environments or teams that need formal SLA guarantees.
Scaleway Kapsule
Simpler and leaner. Kapsule abstracts the control plane entirely and integrates cleanly with Scaleway’s container registry and object storage. Well-suited for product teams that want managed Kubernetes without the operational overhead.
Hetzner + k3s
The most cost-efficient path. Hetzner doesn’t offer a managed Kubernetes product, but the open-source hetzner-k3s tool provisions a k3s cluster on Hetzner Cloud from a single config file. You own the control plane more responsibility, but a strong option for teams comfortable running their own infrastructure.
All three are ISO 27001 certified and GDPR-native, which removes the compliance overhead that comes with hyperscaler data processing agreements.
What We’re Building
A CI/CD pipeline that does the following on every merge to main:
- Runs the test suite
- Builds a Docker image and pushes it to a European container registry
- Scans the image for critical vulnerabilities
- Deploys automatically to staging
- Waits for a manual approval gate before touching production
The sample application is a Python (FastAPI) web service with a PostgreSQL dependency — realistic enough to demonstrate secrets management and health checks without being artificially complex.
The Application Container
A Production Dockerfile
The key principle is a multi-stage build: separate the build environment from the runtime image so you’re not shipping compilers or build tools to production.
dockerfile
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
FROM python:3.12-slim AS runtime
RUN useradd --create-home appuser
WORKDIR /app
COPY /root/.local /home/appuser/.local
COPY app/ ./app/
USER appuser
ENV PATH=/home/appuser/.local/bin:$PATH
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Two things worth noting: the container runs as a non-root user, and only the installed packages — not the build stage — are copied into the final image.
Container Registry Options in Europe
| Registry | Provider |
|---|---|
| OVHcloud Managed Registry | OVHcloud (Harbor-based) |
| Scaleway Container Registry | Scaleway |
| GitLab Container Registry | GitLab.com (included with all plans) |
| GitHub Container Registry | GitHub (ghcr.io) |
For this walkthrough we use the GitLab Container Registry for the GitLab CI pipeline, and ghcr.io for GitHub Actions — both keep the pipeline self-contained within the same platform.
The GitLab CI Pipeline
The pipeline lives in .gitlab-ci.yml at the repository root, with four stages: test, build, deploy-staging, and deploy-production.
Test
yaml
unit-tests:
stage: test
image: python:3.12-slim
script:
- pip install -r requirements.txt pytest
- pytest tests/ -v --tb=short
Runs on every push to every branch. If tests fail, nothing else runs.
Build and scan
yaml
build-image:
stage: build
image: docker:26
services:
- docker:26-dind
only: [main]
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock
aquasec/trivy:latest image --exit-code 1 --severity CRITICAL
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
The Trivy scan runs before the push. A critical CVE found at this stage fails the pipeline — the image never reaches the registry. $CI_REGISTRY_IMAGE, $CI_REGISTRY_USER, and $CI_REGISTRY_PASSWORD are all provided automatically by GitLab.
Deploy to staging (automatic)
yaml
deploy-staging:
stage: deploy-staging
image: dtzar/helm-kubectl:3.14
only: [main]
before_script:
- echo "$KUBE_CONFIG_STAGING" | base64 -d > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
script:
- |
helm upgrade --install my-webapp-staging ./helm/my-webapp \
--namespace staging --create-namespace \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--set env.DATABASE_URL="$STAGING_DATABASE_URL" \
--wait --timeout 5m
Staging deploys automatically on every successful build. The --wait flag means the job only marks as successful once all pods are running and passing their health checks.
Deploy to production (manual gate)
yaml
deploy-production:
stage: deploy-production
image: dtzar/helm-kubectl:3.14
when: manual
environment:
name: production
url: https://my-webapp.example.com
before_script:
- echo "$KUBE_CONFIG_PROD" | base64 -d > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
script:
- |
helm upgrade --install my-webapp-prod ./helm/my-webapp \
--namespace production --create-namespace \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--set env.DATABASE_URL="$PROD_DATABASE_URL" \
--set replicaCount=3 \
--wait --timeout 5m
when: manual is all it takes to require a human to click Deploy in the GitLab UI. The same image that passed tests, scanning, and staging is what gets promoted — no rebuild.
Set these as masked, protected CI variables in Settings → CI/CD → Variables:
| Variable | Description |
|---|---|
KUBE_CONFIG_STAGING |
base64-encoded kubeconfig for staging |
KUBE_CONFIG_PROD |
base64-encoded kubeconfig for production |
STAGING_DATABASE_URL |
PostgreSQL connection string for staging |
PROD_DATABASE_URL |
PostgreSQL connection string for production |
The GitHub Actions Pipeline
The equivalent GitHub Actions workflow mirrors the same four stages. The key difference is OIDC federation for authentication:
yaml
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN is a short-lived token automatically provisioned for each workflow run no long-lived credentials stored anywhere. For cloud provider authentication, OIDC lets GitHub Actions present a signed token that the provider trusts directly.
For the production gate, configure the production environment in Settings → Environments and add required reviewers. Any job targeting that environment pauses until an approval is granted.
Kubernetes Deployment with Helm
Helm is the standard package manager for Kubernetes. The pipeline runs helm upgrade --install, which creates the release on first deploy and updates it on subsequent runs idempotent and rollback-capable.
The values file is where the pipeline injects environment-specific configuration:
yaml
replicaCount: 2
image:
repository: ghcr.io/your-org/my-webapp
tag: latest
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
The --set image.tag=$CI_COMMIT_SHORT_SHA argument overrides latest with the exact SHA that was built and tested — every deployment is traceable to a specific commit.
One thing the Deployment template should always include:
yaml
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
Zero-downtime rolling updates are a one-liner in Kubernetes. There’s no reason not to have them.
Ingress and TLS
Install ingress-nginx and cert-manager via Helm. nginx handles incoming traffic routing; cert-manager watches for Ingress resources and automatically provisions Let’s Encrypt certificates.
Once a ClusterIssuer is configured, all your Ingress resources need is a single annotation:
yaml
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
cert-manager handles the ACME challenge, certificate issuance, and renewal entirely automatically. Point your DNS A record to the LoadBalancer IP and TLS just works.
Security Hardening
Getting the pipeline running is the first milestone. Getting it secure is the requirement before calling it production-ready.
Image
- Multi-stage build with a minimal base image (
python:3.12-slim, notpython:3.12) - Trivy scan in CI with
--exit-code 1onCRITICALseverity - Non-root user inside the container
readOnlyRootFilesystem: truein the pod spec
Secrets
- Database credentials passed via Kubernetes Secrets and mounted as environment variables using
secretKeyRef— not hardcoded in values files - CI secrets scoped to protected branches only and marked as masked
RBAC
- The CI pipeline’s ServiceAccount should have only what it needs:
get,list,updateon Deployments and Services in specific namespaces nothing wider
Network
- NetworkPolicy resources limiting which pods can talk to which
- All external traffic routed through the Ingress controller no NodePort services exposed directly
The Full Deployment Flow
Once everything is in place, a typical deploy cycle looks like this:
Developer merges a PR to main
→ Tests run and pass
→ Docker image is built and scanned (fails on critical CVEs)
→ Image pushed to the European registry
→ Helm deploys to staging automatically
→ Rolling update completes, health checks pass
Engineer reviews staging
→ Approves the production deployment
→ Same image promoted to production
→ Zero-downtime rolling update completes
→ Prometheus begins scraping new pods within 30 seconds
Merge to production in under 10 minutes, with a human gate at the critical step, and every deployment traceable to a Git commit SHA.
Conclusion
The European cloud ecosystem has matured to the point where there’s no meaningful capability gap for running web application workloads. OVHcloud, Scaleway, and Hetzner offer the managed services that production deployments require, and the open-source tooling — Helm, cert-manager, kube-prometheus-stack — is provider-agnostic throughout.
At ADM Cloudtech, this is the infrastructure model we deploy for medium-to-large scale web applications that need to stay on European soil. Data sovereignty, compliance, and engineering quality aren’t trade-offs with the right stack, you get all three.
If you’re evaluating a move to European infrastructure or want to talk through your specific architecture, ADM Cloudtech team is happy to help.
