Skip to main content
No Terraform imports, plans against live infrastructure, or applies were performed when this package was created. Validation was local. Import updates Terraform state; applying a plan changes infrastructure.

Prepare state and access

Use an operator role with short-lived credentials and the target account guard in each root. Do not use AWS root credentials for routine operations or give them to the application. Keep Cloudflare credentials in CLOUDFLARE_API_TOKEN, populated through your secret manager; the token needs Zone Read and DNS Edit scoped to the selected zone. Pre-create a private S3 state bucket with versioning, encryption, public access blocked, and tightly scoped IAM. Terraform state and saved plans can contain sensitive data even when console output is redacted. The backend example enables encryption and native S3 locking; the operator needs Get/Put on the state object and Get/Put/Delete on its .tflock object. With KMS encryption, configure the KMS key and necessary key permissions as well. Do not commit local state, plans, backend configs or real tfvars. Copy terraform/backend.hcl.example to a local state.backend.hcl inside each environment. Use distinct state keys (xem/dokploy/terraform.tfstate, xem/domain/terraform.tfstate, xem/kubernetes/terraform.tfstate). Initialize from each root:
After local validation with -backend=false, a real initialization must explicitly configure the backend. Never assume a local validation state is the production state.

Adopt existing Dokploy infrastructure

From terraform/environments/dokploy, copy production.tfvars.example to production.tfvars, verify the inventory against AWS, and initialize the remote backend. The example is the 2026-09-12 snapshot; it is not live discovery. Import each existing resource once, using the same variable file and state. Skip resources already present in this state; never import the same object into another state.
The guard rejects create/delete/replacement and unreviewed updates. Initial adoption should converge to a no-op plan. If a benign update is intentional, inspect every changed attribute and then use --allow-updates; this still rejects replacement. Do not ignore drift wholesale to make a plan pass. prevent_destroy protects critical resources while their configuration remains present; it does not protect resources removed from configuration/state or prevent a nested CloudFormation template update from replacing a child. Review the CloudFormation template diff too. The imported EC2 root disk is unencrypted, 128 GiB gp3. Adoption preserves it. Encrypting it requires a separate snapshot/volume or instance migration with backups and downtime planning. Changing the AMI, subnet, encryption or disk configuration during adoption can request replacement and is blocked. The existing administrative security group is referenced rather than managed; this root neither opens SSH nor installs Dokploy. Egress on the SMTP SG remains unrestricted so the application can reach external dependencies; tighten it only with a complete dependency inventory. The CloudFormation stack remains the sole owner of its generated SNS topic, runtime policy and conditional subscription. Do not import those children into Terraform resources or create a second copy with guessed names. cloudformation.json mirrors the backend’s managed-sending template. Review both copies together when changing IAM. The runtime role has no static access keys. Keep feedback_endpoint = "" during adoption because that is the existing stack value. After the backend is configured with the exact topic ARN, set the endpoint to https://API_HOST/api/v1/sending/events/ses in a separate reviewed change. Verify SNS subscription confirmation and signed event handling; a successful stack update alone does not prove feedback works.

Platform domain and Cloudflare

This root owns only the platform domain (xem.email). Customer identities belong to Xem and must not be adopted here. It does not change the root domain’s existing mailbox MX, SPF or iCloud DKIM records. From terraform/environments/domain, copy platform.tfvars.example to platform.tfvars, set the zone ID (not the Cloudflare account ID), initialize its separate backend, and authenticate both providers. For existing production records:
Read the actual Cloudflare record IDs and match each DKIM token to its exact record; do not assume the UI order is the provider’s token order. Substitute the verified IDs below:
Defaults: 2048-bit DKIM; bounce.DOMAIN MX and SPF; SES MAIL FROM failure rejects the message. DMARC management is opt-in and starts at p=none; adopting an existing stricter policy requires setting its exact content, never weakening it automatically. An SMTP A record targets the EC2 EIP; a Kubernetes CNAME targets the NLB DNS name. Both must remain DNS-only (proxied=false). Do not move SMTP DNS until certificate, readiness and send tests pass on the destination. For a genuinely new domain, the plan will intentionally create resources and the adoption guard will reject it. Review that creation plan separately rather than pretending it is an import. SES production access and account sending/suppression settings are account-level operations; this module neither requests production access nor resets those policies.

Kubernetes IAM

The Kubernetes root takes an existing IAM OIDC provider and issuer URL. Its exact trust condition must match the Helm namespace and workloadIdentity.serviceAccountName. Use a distinct stack and role name, and a separate state key. See Kubernetes deployment. Do not reuse the live Dokploy stack in both states.