Replace Dokploy with Docker Compose + Traefik + docker-rollout - #837
Conversation
Remove the Dokploy PaaS layer and replace with direct Docker Compose management. Traefik handles TLS via Cloudflare DNS challenge, docker-rollout provides zero-downtime deploys, and GitHub Actions deploys via SSH instead of the Dokploy API. New services: Portainer (portainer.dofek.asherlc.com) for Docker management, Netdata (netdata.dofek.asherlc.com) for server health monitoring — both behind Authentik forward auth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Storybook preview for This comment updates automatically on each PR push. |
Mobile PreviewScan to open on device:
To test on device:
|
There was a problem hiding this comment.
Pull request overview
This PR replaces the Dokploy-based deployment approach with a direct Docker Compose production stack fronted by Traefik and deployed via SSH-driven docker-rollout, while also adding Portainer and Netdata behind Authentik forward-auth.
Changes:
- Remove Dokploy provisioning/config tooling (Terraform modules, setup scripts, secret sync script/tests) and update docs accordingly.
- Add a unified production Compose stack (
deploy/docker-compose.deploy.yml) with Traefik, app services, infra services, backups, and management UIs (Portainer/Netdata). - Rewrite GitHub deploy + secret-sync workflows to SSH into the server, export Infisical secrets to
/opt/dofek/.env.prod, and deploy/restart via Docker Compose / rollout.
Reviewed changes
Copilot reviewed 26 out of 28 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/sync-dokploy-env-from-infisical.ts | Removed Dokploy env sync script (no longer needed). |
| scripts/sync-dokploy-env-from-infisical.test.ts | Removed tests for Dokploy env sync logic. |
| scripts/sync-dokploy-env-from-infisical-lib.ts | Removed Dokploy env merge/parsing library. |
| README.md | Updated infrastructure/deploy documentation from Dokploy → Compose/Traefik/rollout. |
| docker-compose.yml | Simplified dev compose to backing services only. |
| deploy/terraform.tfvars.example | Updated example tfvars to match new server module inputs. |
| deploy/server/main.tf | Updated Hetzner provisioning to install Docker + rollout + Infisical (no Dokploy). |
| deploy/server/cloud-init.yml | New cloud-init for Docker/rollout/Infisical installation and host setup. |
| deploy/otel-collector-config.yaml | Added/updated collector config for OTLP + Docker filelog shipping. |
| deploy/dokploy/terraform.tfvars.example | Removed Dokploy tfvars example. |
| deploy/dokploy/setup.sh | Removed Dokploy setup script (tRPC-based). |
| deploy/dokploy/managed-secret-keys.json | Removed Dokploy-managed secret allowlist. |
| deploy/dokploy/infra-compose.yml | Removed Dokploy infra compose stack template. |
| deploy/dokploy/cloud-init.yml | Removed Dokploy installer cloud-init. |
| deploy/dokploy/.gitignore | Removed Dokploy-specific gitignore. |
| deploy/dokploy-compose/main.tf | Removed Terraform template renderer for Dokploy infra compose. |
| deploy/dokploy-app/main.tf | Removed Terraform-based Dokploy app deploy module. |
| deploy/docker-compose.deploy.yml | New production Compose stack with Traefik, apps, infra, backups, Portainer, Netdata. |
| deploy/db/main.tf | New Terraform module for managing the TimescaleDB extension via SSH tunnel. |
| deploy/cloudflare/main.tf | Added DNS records for Portainer/Netdata; updated comments/outputs. |
| AGENTS.md | Updated “no manual server changes” guidance to reflect Compose/Terraform workflow. |
| .github/workflows/secret-sync.yml | Reworked secret sync to SSH + infisical export → /opt/dofek/.env.prod + restarts. |
| .github/workflows/preview-deploy.yml | Removed Dokploy-based preview deploy workflow. |
| .github/workflows/deploy.yml | Updated cleanup to SSH into server and prune images. |
| .github/workflows/deploy-worker.yml | Updated worker deploy to SSH + pull + rollout-based deploy. |
| .github/workflows/deploy-web.yml | Updated web deploy to SSH + pull + rollout-based deploy. |
| .github/workflows/deploy-infra-compose.yml | Updated infra deploy to scp compose/config + docker compose pull/up + DB extension apply. |
| .github/workflows/ci.yml | Updated infra validation + change-detection triggers for new deploy layout. |
Comments suppressed due to low confidence (2)
deploy/server/main.tf:103
user_datais rendered fromcloud-init.ymlwithghcr_tokenand will end up stored in Terraform state (and potentially visible via Hetzner/user-data APIs). Avoid embedding registry PATs in cloud-init/Terraform state; instead performdocker loginduring deploy using ephemeral credentials, or use a pull token stored on the host outside Terraform state (e.g., created manually and referenced via file/1Password/Infisical on the server).
docker-compose.yml:46- This dev compose mounts
/opt/dofek/otel-collector-config.yamlfrom the host, which typically won’t exist on developer machines and can cause the collector container to fail to start (Docker will mount a directory instead of the file). For local dev, mount the repo’sdeploy/otel-collector-config.yaml(or make the mount optional) sodocker compose up -dworks as documented.
collector:
image: otel/opentelemetry-collector-contrib:0.149.0
restart: unless-stopped
user: "0:0"
environment:
AXIOM_API_TOKEN: ${AXIOM_API_TOKEN}
SENTRY_OTLP_LOGS_ENDPOINT: ${SENTRY_OTLP_LOGS_ENDPOINT:-}
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /opt/dofek/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:ro
healthcheck:
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - mkdir -p /usr/local/lib/docker/cli-plugins | ||
| - | | ||
| ARCH=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/amd64/') | ||
| curl -fsSL "https://github.com/Wowu/docker-rollout/releases/latest/download/docker-rollout-linux-$${ARCH}" \ | ||
| -o /usr/local/lib/docker/cli-plugins/docker-rollout | ||
| - chmod +x /usr/local/lib/docker/cli-plugins/docker-rollout |
There was a problem hiding this comment.
The docker-rollout plugin is fetched from the releases/latest URL without any version pinning or checksum/signature verification. For reproducible and safer provisioning, pin a specific docker-rollout version and verify the downloaded binary (e.g., sha256) before installing.
| - mkdir -p /usr/local/lib/docker/cli-plugins | |
| - | | |
| ARCH=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/amd64/') | |
| curl -fsSL "https://github.com/Wowu/docker-rollout/releases/latest/download/docker-rollout-linux-$${ARCH}" \ | |
| -o /usr/local/lib/docker/cli-plugins/docker-rollout | |
| - chmod +x /usr/local/lib/docker/cli-plugins/docker-rollout | |
| - | | |
| set -eu | |
| mkdir -p /usr/local/lib/docker/cli-plugins | |
| ARCH=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/amd64/') | |
| DOCKER_ROLLOUT_VERSION="v0.6.0" | |
| case "$${ARCH}" in | |
| amd64) | |
| DOCKER_ROLLOUT_SHA256="REPLACE_WITH_OFFICIAL_AMD64_SHA256" | |
| ;; | |
| arm64) | |
| DOCKER_ROLLOUT_SHA256="REPLACE_WITH_OFFICIAL_ARM64_SHA256" | |
| ;; | |
| *) | |
| echo "Unsupported architecture: $${ARCH}" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| curl -fsSL "https://github.com/Wowu/docker-rollout/releases/download/$${DOCKER_ROLLOUT_VERSION}/docker-rollout-linux-$${ARCH}" \ | |
| -o /usr/local/lib/docker/cli-plugins/docker-rollout | |
| echo "$${DOCKER_ROLLOUT_SHA256} /usr/local/lib/docker/cli-plugins/docker-rollout" | sha256sum -c - | |
| chmod +x /usr/local/lib/docker/cli-plugins/docker-rollout |
There was a problem hiding this comment.
Fixed in 2b35765d. Pinned to v0.13 (it's a shell script, not a compiled binary, so no checksum needed — but version pinning is still important for reproducibility).
| - chmod +x /usr/local/lib/docker/cli-plugins/docker-rollout | ||
|
|
||
| # Install Infisical CLI | ||
| - curl -1sLf 'https://artifacts.infisical.com/repos/apt/setup.deb.sh' | bash |
There was a problem hiding this comment.
Infisical is installed via a remote setup.deb.sh | bash pipeline. This makes server provisioning non-reproducible and increases supply-chain risk; prefer adding Infisical’s apt repo key + source explicitly (or pin a .deb version) rather than executing a remote script.
| - curl -1sLf 'https://artifacts.infisical.com/repos/apt/setup.deb.sh' | bash | |
| - install -m 0755 -d /etc/apt/keyrings | |
| - curl -fsSL https://artifacts.infisical.com/gpg/infisical-archive-keyring.asc -o /etc/apt/keyrings/infisical.asc | |
| - chmod a+r /etc/apt/keyrings/infisical.asc | |
| - echo "deb [signed-by=/etc/apt/keyrings/infisical.asc] https://artifacts.infisical.com/repos/apt $(. /etc/os-release && echo $VERSION_CODENAME) main" > /etc/apt/sources.list.d/infisical.list | |
| - apt-get update |
There was a problem hiding this comment.
Fixed in 2b35765d. Switched to explicit GPG key + apt source list, matching the Docker install pattern above it.
| # GHCR login for image pulls | ||
| - echo "${ghcr_token}" | docker login ghcr.io -u "${ghcr_username}" --password-stdin |
There was a problem hiding this comment.
Logging into GHCR in cloud-init using a PAT risks leaking the token via cloud-init logs (/var/log/cloud-init-output.log) and makes rotation harder. Consider moving registry login to the deploy workflow (ssh step) or storing credentials on the host in a root-only Docker config created out-of-band (not in user_data).
| # GHCR login for image pulls | |
| - echo "${ghcr_token}" | docker login ghcr.io -u "${ghcr_username}" --password-stdin | |
| # Do not log into GHCR from cloud-init: perform registry authentication | |
| # during deployment (for example, in the SSH deploy step) or provide a | |
| # root-only Docker config out-of-band so credentials are not embedded in | |
| # user-data or exposed via cloud-init logs. |
There was a problem hiding this comment.
Fixed in 2b35765d. Removed GHCR login from cloud-init entirely. Registry auth now happens during deploy workflows via SSH, keeping credentials out of user-data, cloud-init logs, and Terraform state.
| ssh "root@${SERVER_HOST}" bash -s -- "$INFISICAL_TOKEN" "$DRY_RUN" <<'SYNC' | ||
| set -euo pipefail | ||
| INFISICAL_TOKEN="$1" | ||
| DRY_RUN="$2" | ||
|
|
||
| # Export secrets from Infisical to .env file | ||
| infisical export --env=prod --format=dotenv \ | ||
| --token="$INFISICAL_TOKEN" \ | ||
| > /opt/dofek/.env.prod.new |
There was a problem hiding this comment.
The Infisical token is passed to the server as an SSH argument and then used via --token=.... Both patterns can expose the token in process listings / audit logs on the server. Prefer using INFISICAL_TOKEN as an environment variable (if supported by the CLI) and avoid passing secrets as positional args to ssh (e.g., send via stdin or use pre-provisioned server-side auth).
There was a problem hiding this comment.
Fixed in 2b35765d. Now using ssh -o SendEnv=INFISICAL_TOKEN so the token is passed as an environment variable rather than a positional argument. The Infisical CLI reads it from the env automatically.
| container_name: dofek-portainer | ||
| restart: unless-stopped | ||
| volumes: | ||
| - /var/run/docker.sock:/var/run/docker.sock:ro |
There was a problem hiding this comment.
Portainer is mounted with the Docker socket read-only. That will prevent most management actions (restarts, exec, container updates) which defeats the purpose of Portainer as a management UI. If you intend Portainer to manage the host, mount /var/run/docker.sock read-write (or explicitly document that Portainer is view-only).
| - /var/run/docker.sock:/var/run/docker.sock:ro | |
| - /var/run/docker.sock:/var/run/docker.sock |
There was a problem hiding this comment.
Fixed in 2b35765d. Removed :ro — Portainer needs read-write access for container management (restart, exec, etc.).
| # Zero-downtime rollout | ||
| docker compose --env-file .env.prod --env-file .env.deploy \ | ||
| -f docker-compose.deploy.yml rollout web |
There was a problem hiding this comment.
docker-rollout is installed as a docker CLI plugin (docker rollout ...), but this workflow invokes it as docker compose ... rollout web. Unless you’ve added a Compose subcommand extension, this will fail at deploy time. Update the deploy command to use the docker-rollout CLI as intended (and ensure it’s available on the server).
| # Zero-downtime rollout | |
| docker compose --env-file .env.prod --env-file .env.deploy \ | |
| -f docker-compose.deploy.yml rollout web | |
| # Ensure the docker-rollout CLI plugin is available on the server | |
| docker rollout --help >/dev/null | |
| # Zero-downtime rollout | |
| set -a | |
| . ./.env.prod | |
| . ./.env.deploy | |
| set +a | |
| docker rollout -f docker-compose.deploy.yml web |
There was a problem hiding this comment.
Fixed in 2b35765d. Changed to docker rollout -f docker-compose.deploy.yml web — docker-rollout is a Docker CLI plugin, not a compose subcommand. Good catch, this would have failed at deploy time.
| docker compose --env-file .env.prod --env-file .env.deploy \ | ||
| -f docker-compose.deploy.yml rollout worker |
There was a problem hiding this comment.
docker-rollout is installed as a docker CLI plugin (docker rollout ...), but this workflow invokes it as docker compose ... rollout worker. Unless you’ve added a Compose subcommand extension, this will fail at deploy time. Update the deploy command to use the docker-rollout CLI as intended (and ensure it’s available on the server).
| docker compose --env-file .env.prod --env-file .env.deploy \ | |
| -f docker-compose.deploy.yml rollout worker | |
| docker rollout --env-file .env.prod --env-file .env.deploy \ | |
| -f docker-compose.deploy.yml worker |
There was a problem hiding this comment.
Fixed in 2b35765d. Same fix as web — docker rollout -f docker-compose.deploy.yml worker.
Resolve conflicts: - docker-compose.yml: keep our version (no OTA/dokploy-network in dev) - deploy/dokploy/infra-compose.yml: keep deletion (replaced by deploy compose) - Pick up OTA image pin to v2.3.16 from main Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix docker-rollout CLI invocation: `docker rollout -f` not `docker compose rollout` - Pin docker-rollout to v0.13 (it's a shell script, no arch-specific binary) - Install Infisical via explicit GPG key + apt repo, not pipe-to-bash - Move GHCR login from cloud-init to deploy workflows (avoids token in logs/TF state) - Pass Infisical token via SendEnv, not positional arg (avoids process listing exposure) - Remove :ro from Portainer docker socket mount (needs rw for management) - Fix dev compose OTel config path to use repo-relative path Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CR SummaryAll 7 comments addressed in Fixed
Also fixed the suppressed comment about dev compose OTel config path ( |
The OTA service moved from docker-compose.yml (dev) to deploy/docker-compose.deploy.yml (production). Update the test to read the correct file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
portainer.dofek.asherlc.com) and Netdata (netdata.dofek.asherlc.com) for Docker management and server health monitoring, both behind Authentik forward authdocker rolloutinstead of Terraform + Dokploy API/opt/dofek/.env.prodon the server, eliminating the Terraform templatefile secret injectionTest plan
terraform validatepasses fordeploy/server/anddeploy/db/deploy/docker-compose.deploy.ymlsyntax:docker compose -f deploy/docker-compose.deploy.yml configcd deploy/server && terraform applydocker rollout webwith concurrent requestsdocker compose up -dstarts db, redis, collector🤖 Generated with Claude Code