From 31784df55fb68efca1b41dea238c0ee65128fcd0 Mon Sep 17 00:00:00 2001 From: Daniele Pintore Date: Tue, 9 Sep 2025 23:45:25 +0200 Subject: [PATCH 01/69] docs: add instructions on how to use cloudflare origin cert --- .../content/docs/core/domains/cloudflare.mdx | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/docs/core/domains/cloudflare.mdx b/apps/docs/content/docs/core/domains/cloudflare.mdx index 61c353a..f553dc6 100644 --- a/apps/docs/content/docs/core/domains/cloudflare.mdx +++ b/apps/docs/content/docs/core/domains/cloudflare.mdx @@ -39,8 +39,41 @@ To switch between modes, follow these steps: Follow the steps in the same order to prevent any issues. -We assume that you have enabled the `Full (Strict)` mode in the previous step, is super important to follow the steps in the same order to prevent any issues. +You can create a certificate for your origin server using two methods: +- Using Cloudflare's Origin CA to generate a certificate for your origin server. +- Using Let's Encrypt to generate a certificate for your origin server. +We assume that you have enabled the `Full (Strict)` mode in the previous step, is super important to follow the steps in the same order to prevent any issues. +### Using Cloudflare's Origin CA +1. Go to cloudflare dashboard and then click on `Account Home` -> Select the Domain. +2. On the left side, click `SSL/TLS`. +3. Click on `Origin Server`. +4. Click on `Create Certificate`. +5. Select `Generate private key and CSR with Cloudflare`. +6. Choose the list of hostnames you want the certificate to cover eg. `api.dokploy.com`. +7. Choose the validity period eg. `15 years`. +8. Click `Create`. +9. Using the PEM format, copy the `Origin Certificate` and `Private Key` in the respective fields in the dokploy new certificate panel (Certificates > Add Certificate). +10. Go to `Domains` section in your application. +11. Click `Create Domain`. +12. In the `Host` field, enter the domain name eg. `api.dokploy.com`. (Make sure that the domain is already pointing to your server IP in Cloudflare DNS settings and the **hostname matches the one in the certificate**). +13. In the `Path` field, enter the path eg. `/`. +14. In the `Container Port` field, enter the port where your application is running eg. `3000`. +15. In the `HTTPS` field enable `ON`. +16. In the `Certificate` field select `None`. +17. Click `Create`. + +Using Cloudflare's Origin CA, you are sure that the certificate will be valid for the next 15 years, or the duration you selected, and you don't have to worry about failed renewals. + + +You can also create a certificate for wildcards domains eg. `*.dokploy.com` and use it for multiple subdomains. + + + +**Important**: With a free Cloudflare account, this methods work only for the main domain and subdomains, not for sub-subdomains. Eg. `api.dokploy.com` works but `staging.api.dokploy.com` does not work. + + +### Using Let's Encrypt 1. Go to cloudflare dashboard and then click on `Account Home` -> Select the Domain. 2. On the left side, click `DNS`. 3. Click on `Records`. From cb5b2cc95922fed8866194174bb2e51762661f56 Mon Sep 17 00:00:00 2001 From: HarikrishnanD Date: Thu, 11 Sep 2025 19:15:33 +0530 Subject: [PATCH 02/69] docs: remove references to removed self-hosted registry feature - Remove misleading self-hosted registry section from cluster docs - Update registry configuration to focus on external registries only - Add comprehensive recommendations for easy-to-setup external registries - Fixes issue where docs mentioned non-existent auto-setup feature. #2575 --- apps/docs/content/docs/core/cluster.mdx | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/docs/content/docs/core/cluster.mdx b/apps/docs/content/docs/core/cluster.mdx index 134e801..72d971b 100644 --- a/apps/docs/content/docs/core/cluster.mdx +++ b/apps/docs/content/docs/core/cluster.mdx @@ -38,24 +38,20 @@ If you choose the second option, we will proceed to configure the different serv To start, we need to configure a Docker registry, as when deploying an application, you need a registry to deploy and download the application image on the other servers. -We offer two ways to configure a registry: - -1. **External Registry**: Use any registry you want. -2. **Self-Hosted Registry**: We create and configure a self-hosted registry for you. - ### External Registry -You can use any registry, such as Docker Hub, DigitalOcean Spaces, ECR, or your choice. Make sure to enter the correct credentials and test the connection before adding the registry. +You can use any external registry of your choice. Here are some popular options: -### Self-Hosted Registry +1. **Docker Hub** - Free tier available, easy to set up +2. **GitHub Container Registry (ghcr.io)** - Free for public repositories +3. **DigitalOcean Container Registry** - Simple setup with good integration +4. **Amazon ECR** - AWS's managed container registry +5. **Google Container Registry** - Google Cloud's managed registry +6. **Azure Container Registry** - Microsoft's managed registry -We will ask you for three things: +Make sure to enter the correct credentials and test the connection before adding the registry to your cluster configuration. -1. A user. -2. A password. -3. A domain. Ensure this domain is pointing to the dokploy VPS. - -Once set up, the Cluster section will be unlocked. +Once configured, the Cluster section will be unlocked. ## Understanding Docker Swarm From 447ad1ad30c0f954beb55f073c447084a64fb2a1 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 6 Oct 2025 22:41:05 -0600 Subject: [PATCH 03/69] fix: update statistics values for GitHub stars, Docker downloads, and contributors --- apps/website/app/[locale]/contact/page.tsx | 308 +++++++++++++++++++++ apps/website/app/api/contact/route.ts | 112 ++++++++ apps/website/components/Header.tsx | 20 ++ apps/website/components/stats.tsx | 6 +- apps/website/components/ui/dialog.tsx | 122 ++++++++ apps/website/locales/en.json | 50 +++- apps/website/locales/es.json | 50 +++- apps/website/locales/fr.json | 50 +++- apps/website/locales/zh-Hans.json | 50 +++- apps/website/package.json | 1 + pnpm-lock.yaml | 14 + 11 files changed, 776 insertions(+), 7 deletions(-) create mode 100644 apps/website/app/[locale]/contact/page.tsx create mode 100644 apps/website/app/api/contact/route.ts create mode 100644 apps/website/components/ui/dialog.tsx diff --git a/apps/website/app/[locale]/contact/page.tsx b/apps/website/app/[locale]/contact/page.tsx new file mode 100644 index 0000000..a562935 --- /dev/null +++ b/apps/website/app/[locale]/contact/page.tsx @@ -0,0 +1,308 @@ +"use client"; + +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { Container } from "@/components/Container"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { trackGAEvent } from "@/components/analitycs"; +import AnimatedGridPattern from "@/components/ui/animated-grid-pattern"; +import { cn } from "@/lib/utils"; + +interface ContactFormData { + inquiryType: "" | "support" | "sales" | "other"; + name: string; + email: string; + company: string; + message: string; +} + +export default function ContactPage() { + const t = useTranslations("Contact"); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + const [formData, setFormData] = useState({ + inquiryType: "", + name: "", + email: "", + company: "", + message: "", + }); + const [errors, setErrors] = useState>({}); + + const validateForm = (): boolean => { + const newErrors: Record = {}; + + if (!formData.inquiryType) { + newErrors.inquiryType = t("errors.inquiryTypeRequired"); + } + if (!formData.name.trim()) { + newErrors.name = t("errors.nameRequired"); + } + if (!formData.email.trim()) { + newErrors.email = t("errors.emailRequired"); + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = t("errors.emailInvalid"); + } + if (!formData.company.trim()) { + newErrors.company = t("errors.companyRequired"); + } + if (!formData.message.trim()) { + newErrors.message = t("errors.messageRequired"); + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + setIsSubmitting(true); + + try { + const response = await fetch("/api/contact", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + if (response.ok) { + // Track successful form submission + trackGAEvent({ + action: "Contact Form Submitted", + category: "Contact", + label: formData.inquiryType, + }); + + // Reset form and show success + setFormData({ + inquiryType: "", + name: "", + email: "", + company: "", + message: "", + }); + setErrors({}); + setIsSubmitted(true); + } else { + throw new Error("Failed to submit form"); + } + } catch (error) { + console.error("Error submitting form:", error); + alert(t("errorMessage")); + } finally { + setIsSubmitting(false); + } + }; + + const handleInputChange = (field: keyof ContactFormData, value: any) => { + setFormData((prev) => ({ ...prev, [field]: value })); + // Clear error when user starts typing + if (errors[field]) { + setErrors((prev) => { + const newErrors = { ...prev }; + delete newErrors[field]; + return newErrors; + }); + } + }; + + if (isSubmitted) { + return ( +
+ +
+

+ {t("successTitle")} +

+

+ {t("successMessage")} +

+
+ +
+
+
+
+ ); + } + + return ( +
+ + +
+
+

+ {t("title")} +

+

+ {t("description")} +

+
+ +
+
+ + + {errors.inquiryType && ( +

{errors.inquiryType}

+ )} +
+ +
+
+ + handleInputChange("name", e.target.value)} + placeholder={t("fields.name.placeholder")} + /> + {errors.name && ( +

{errors.name}

+ )} +
+ +
+ + handleInputChange("email", e.target.value)} + placeholder={t("fields.email.placeholder")} + /> + {errors.email && ( +

{errors.email}

+ )} +
+
+ +
+ + handleInputChange("company", e.target.value)} + placeholder={t("fields.company.placeholder")} + /> + {errors.company && ( +

{errors.company}

+ )} +
+ +
+ +