Portainer is our default ops UI for Docker Compose on dardev-vps; kubectl is what we reach for on Kubernetes clusters and when Compose debugging needs raw API access. The choice is not ideology—it is which runtime you are touching. Twenty CRM, Listmonk, Stalwart MTA, and the Twenty→Listmonk sync worker on the shared VPS are Compose stacks managed through Portainer after GitLab CI pushes images. DarDevLab homework clusters, client production namespaces, and Flux-driven internal environments stay in kubectl and GitOps workflows.
Teams that conflate the two tools usually inherit a slide deck that says everything runs on Kubernetes. Our mailer stack on dardev-vps deliberately does not run Kubernetes on that host. When a client outgrows Compose, we migrate to K8s and shift daily ops to kubectl and GitOps—not Portainer pretending to be a cluster manager.
What runs where on dardev-vps
dardev-vps at 51.75.205.103 hosts dardev-mailer at /opt/dardev-mailer. Host nginx terminates TLS for crm.dardev.net, mail.dardev.net, news.dardev.net, and mta.dardev.net; compose nginx stays disabled. Twenty binds 127.0.0.1:3001, Listmonk 9002, sync worker 3100, Stalwart admin 8080—documented in our deployment runbooks.
- Portainer: stack health, container logs, image pull after CI, emergency restart
- Git plus deploy scripts: intentional changes to compose files and.env
- kubectl: not the daily driver on this VPS—reserved for lab clusters elsewhere
- Grafana: correlates Portainer-visible restarts with metrics—see grafana-dashboards-small-teams
When we open Portainer
Daily ops rotation uses Portainer because it answers questions a platform engineer asks every morning without memorizing twelve docker compose project names. Is Twenty healthy? Did Listmonk pull the image tag CI built? Is Stalwart queue depth visible in logs? Click, scroll, restart unhealthy container, escalate if restart loops.
Onboarding follows integrations-we-actually-run: week one read-only GitLab and Grafana; week two Portainer logs and shadow deploy. No cluster-admin kubeconfig for Compose—there is no cluster.
- Verify image tag after merge to main triggered deploy
- Tail logs when Grafana error rate spikes but SSH is inconvenient
- Restart a single service after OOM without bringing down postgres
When we use kubectl instead
kubectl enters when the control plane is Kubernetes. DarDevLab assignments expect students to kubectl get pods, describe failures, and patch Deployments. Client engagements on managed or self-hosted K8s use kubectl for rollout status, cordon and drain planning, and CRD-backed tooling—cert-manager, ingress controllers, Flux Kustomizations. Those paths are documented in gitlab-ci-kubernetes-pipeline and gitops-flux-lessons on this site.
kubectl still appears for kind or k3s labs, one-off manifests Helm cannot express, and teaching the object model Portainer hides. Teams that only learn click-ops freeze on the first CrashLoopBackOff in a client namespace.
Compose incidents: Portainer logs and restart; bad image means git revert plus deploy script. K8s regressions: kubectl rollout undo or Flux revert per runbook-rollback-bad-deploy.
Decision table we give clients
Prospects often ask which tool to standardize on. We answer with runtime first, team size second, GitOps maturity third.
- Single VPS, fewer than eight services, one ops part-timer → Compose plus Portainer plus git deploys
- Multiple tenants, autoscaling, or CRD-heavy platform → Kubernetes plus kubectl plus GitOps
- Hybrid → Portainer on VPS edge stacks, kubectl on cluster core; never duplicate config in both UIs
MENA teams often already run GitLab on a modest OVH VPS before they need a cluster. dardev-vps-style Compose is honest capacity planning—we publish live versus roadmap limits in deployed-realities-explained.
Permissions and failure modes
Portainer admin is limited to ops rotation—not every developer with CRM access. GitLab protected branches gate production deploy scripts. kubectl RBAC on client clusters separates namespace editors from cluster admins; CI service accounts never receive cluster-admin tokens.
- CI deploy succeeds but Portainer shows old digest—registry cache or compose pull not run
- kubectl rollout healthy while app returns 500—readiness probe too shallow
- Manual Portainer env var not in git—drift until next deploy overwrites
- Wrong namespace deleted—RBAC and staging rehearsal prevent this
First runbook commands: git log, docker compose ps or Portainer stack view, kubectl rollout history—label the runtime before mixing tools.
inside-unified-mailer-stack covers Twenty→Listmonk sync; Portainer restarts sync-worker when webhooks stall without replacing git-backed worker logic.

What we do not claim
- Portainer as GitOps engine—Flux and git remain authoritative on clusters
- kubectl for routine Compose restarts—docker compose and Portainer are faster
- One tool for every client—runtime and team skill decide
- Multi-region active-active on dardev-vps—single VPS with backups matches our scale today
DarDev Services scopes the same split for clients: GitLab CI, Portainer or kubectl runbooks, Grafana baselines. Overview at dardev.net/products—contact routes to Twenty CRM.
Should we manage dardev-vps Compose stacks with kubectl?
No. There is no Kubernetes API on that host for mailer stacks. Use git-backed compose files, deploy scripts, and Portainer for visibility. kubectl applies when you operate an actual cluster.
Can Portainer replace kubectl on client Kubernetes?
Portainer can attach to clusters, but we still teach kubectl for rollouts, CRDs, and GitOps debugging. UI convenience does not remove the need for YAML literacy under pressure.
What is the first step after a bad deploy on Compose?
Check Portainer for restarting containers and image tag, then follow runbook-rollback-bad-deploy: git revert plus redeploy script—not ad hoc container edits that drift from repo.
How do new operators learn both tools?
Follow integrations-we-actually-run access tiers, then kubectl labs on kind before client namespace access.
When should we move from Portainer to kubectl full-time?
When you need autoscaling, multi-tenant isolation, or platform CRDs—not when a blog post says Kubernetes is mandatory. Migrate workloads first; tooling follows the runtime.



