Compare commits

...

83 Commits

Author SHA1 Message Date
Mauricio Siu
acd722678e Merge pull request #521 from Dokploy/canary
v0.9.4
2024-10-03 02:12:03 -06:00
Mauricio Siu
3750977f41 Merge pull request #520 from Dokploy/487-private-docker-container-pull-failed-despite-having-docker-registry-configured-in-registry
fix(registry): add option to login the registry in the remote server
2024-10-03 02:02:51 -06:00
Mauricio Siu
9b401059b0 fix(registry): add option to login the registry in the remote server 2024-10-03 01:56:50 -06:00
Mauricio Siu
6a3ef5c860 Merge pull request #519 from Dokploy/514-failing-to-refresh-docker-composeyml-from-github-repo
fix(compose): delete content when is remote server
2024-10-03 01:32:50 -06:00
Mauricio Siu
a5eb4b0a72 fix(compose): delete content when is remote server 2024-10-03 01:00:35 -06:00
Mauricio Siu
b5c0876dd4 Merge pull request #518 from Dokploy/515-non-admin-users-are-not-able-to-set-up-database-backup
fix(destinations): change admin to protected procedure
2024-10-03 00:53:23 -06:00
Mauricio Siu
9745d12ac8 fix(destinations): change admin to protected procedure 2024-10-03 00:48:00 -06:00
Mauricio Siu
4aaf04ce74 Merge pull request #506 from AprilNEA/fix/domin-port-number-convert
Fix port input value becoming NaN
2024-10-02 13:10:17 -06:00
Mauricio Siu
ecfca9419a refactor: remove innecessary conversion 2024-10-02 13:02:20 -06:00
AprilNEA
dfd6764320 styles: format code with prettier 2024-10-02 18:22:21 +00:00
Mauricio Siu
73bf5274f5 chore(version): bump version 2024-10-01 14:28:11 -06:00
AprilNEA
fc38a42587 fix: convert final value 2024-10-01 14:36:45 +00:00
Mauricio Siu
9b255964fe Merge pull request #511 from Dokploy/509-create-compose-modal-remains-open-after-clicking-create
509 create compose modal remains open after clicking create
2024-09-30 21:33:56 -06:00
Mauricio Siu
29f55ca1a0 Merge pull request #496 from missuo/canary
feat: add update option
2024-09-30 15:04:03 -06:00
Mauricio Siu
6a5fb8faff fix(multi-server): show the servers ip instead of the main ip #502 2024-09-30 15:00:32 -06:00
Mauricio Siu
5c225c8d42 fix(modal): close the modal after the creation #509 2024-09-30 15:00:01 -06:00
AprilNEA
c1c5fc978b fix: fix number convert when string empty 2024-09-30 08:35:49 +00:00
Vincent Yang
18b4b23f79 feat: add update option for canary and feature tag 2024-09-29 22:45:39 -04:00
Mauricio Siu
727e50648e Merge pull request #501 from Dokploy/canary
v0.9.3
2024-09-29 16:30:43 -06:00
Mauricio Siu
0b2b20caeb chore(version): bump version 2024-09-29 16:24:35 -06:00
Mauricio Siu
6cc64b4454 refactior(terminal): add port to server connect 2024-09-29 16:24:09 -06:00
Mauricio Siu
349bc89851 Merge pull request #500 from Dokploy/canary
v0.9.2
2024-09-29 14:16:56 -06:00
Mauricio Siu
7046d05f63 Merge pull request #499 from Dokploy/fix/update-ports-validation-
fix(multi-server): remove string validation on port
2024-09-29 14:07:00 -06:00
Mauricio Siu
cef21ac8b5 fix(multi-server): remove string validation on port 2024-09-29 13:31:38 -06:00
Vincent Young
7027f39c48 feat: add update option 2024-09-28 15:06:20 -04:00
Mauricio Siu
9f6f872536 Merge pull request #495 from Dokploy/canary
v0.9.1
2024-09-28 12:00:04 -06:00
Mauricio Siu
cb03b153ac Merge pull request #494 from Dokploy/fix/swagger-token
Fix/swagger token
2024-09-28 02:31:39 -06:00
Mauricio Siu
e5d7a0cb10 Merge pull request #493 from Dokploy/491-bug-nixpacks-publish-directory-issues-with-multi-level-paths
fix(nixpacks): adjust build path on nixpacks static
2024-09-28 02:30:29 -06:00
Mauricio Siu
bf65bc9462 chore(lint): format 2024-09-28 02:25:06 -06:00
Mauricio Siu
b48b9765cd fix(nixpacks): adjust build path on nixpacks static 2024-09-28 02:23:30 -06:00
Mauricio Siu
7cce02f74d Merge pull request #489 from Dokploy/docs/488-env-editor-not-support-multiline-variables
docs: add explanation how to use multiline env variables
2024-09-27 22:38:36 -06:00
Ben
3dc3672406 docs: add explanation how to use multiline env variables 2024-09-27 19:30:41 +02:00
Mauricio Siu
bbfe095045 chore(version): bump version 2024-09-27 11:23:44 -06:00
Mauricio Siu
65c1001751 fix(swagger): add mising validation 2024-09-27 11:22:52 -06:00
Mauricio Siu
5212bde021 Merge pull request #486 from Dokploy/333-add-flexible-wwwnon-www-redirect-option
#333 add flexible www/non-www redirect option
2024-09-27 00:42:30 -06:00
Ben
9059f42b03 refactor: display certificate select field after https switch field inside add-domain dialogs 2024-09-26 13:53:10 +02:00
Ben
3b9f5d6f5c feat: add presets for add-redirect dialog 2024-09-26 13:14:14 +02:00
Mauricio Siu
21dee4abac refactor: update social networks 2024-09-22 20:03:48 -06:00
Mauricio Siu
e378d89477 Merge pull request #475 from Dokploy/canary
v0.9.0
2024-09-22 19:38:12 -06:00
Mauricio Siu
b04c1206e4 refactor(multi-server): update logs 2024-09-22 19:28:19 -06:00
Mauricio Siu
639bc0e8db chore(version): bump version 2024-09-22 19:17:39 -06:00
Mauricio Siu
9a850d388d Merge pull request #453 from mpcref/patch-1
Generate valid appName on changing name
2024-09-22 19:07:09 -06:00
Mauricio Siu
6c5c374139 feat(docs): add multi server docs and troubleshooting 2024-09-22 18:54:32 -06:00
Mauricio Siu
0b05f8b83c Merge pull request #469 from Dokploy/139-multi-server-feature
139 multi server feature
2024-09-22 17:20:26 -06:00
Mauricio Siu
63d5b775e6 refactor(multi-server): add config 2024-09-22 17:18:07 -06:00
Mauricio Siu
cb16de63df refactor(multi-server): copy the right value 2024-09-22 17:12:28 -06:00
Mauricio Siu
31a4a0814e refactor: remove logs 2024-09-22 16:51:31 -06:00
Mauricio Siu
14302ed240 refactor: remove imports 2024-09-22 16:49:07 -06:00
Mauricio Siu
31c55f772d refactor(multi-server): remove logs 2024-09-22 16:37:57 -06:00
Mauricio Siu
63e7eacae9 chore(version): bump version 2024-09-19 16:37:00 -06:00
Mauricio Siu
f4ab588516 Merge pull request #466 from Dokploy/canary
v0.8.3
2024-09-19 16:01:27 -06:00
Mauricio Siu
4d8a0ba58f Merge pull request #457 from Dokploy/canary
v0.8.2
2024-09-16 15:57:20 -06:00
Michiel Crefcoeur
847d6ecab1 add trailing comma's 2024-09-16 22:21:12 +02:00
Michiel Crefcoeur
8f83ecb9ef formatting 2024-09-16 22:20:02 +02:00
Michiel Crefcoeur
2f9448dde9 corrections 2024-09-16 22:13:30 +02:00
Michiel Crefcoeur
e1ec0aee69 replaceAll 2024-09-16 10:50:46 -07:00
Michiel Crefcoeur
7f378b12ae and for database 2024-09-16 07:37:23 -07:00
Michiel Crefcoeur
fac984d299 same thing for compose 2024-09-16 07:35:59 -07:00
Michiel Crefcoeur
4f3eb7b362 Generate valid appName on changing name
The same can probably also be done at other places. Should probably be solved in a generic way.
2024-09-16 07:25:16 -07:00
Mauricio Siu
e88cd11041 Merge pull request #427 from Dokploy/canary
v0.8.1
2024-09-07 13:25:36 -06:00
Mauricio Siu
5f174a883b Merge pull request #424 from Dokploy/canary
v0.8.0
2024-09-07 00:55:08 -06:00
Mauricio Siu
536a6ba2ff Merge pull request #397 from Dokploy/canary
v0.7.3
2024-08-30 00:27:59 -06:00
Mauricio Siu
213fa08210 Merge pull request #382 from Dokploy/canary
v0.7.2
2024-08-26 15:52:49 -06:00
Mauricio Siu
d5c6a601d8 Merge pull request #367 from Dokploy/canary
v0.7.1
2024-08-19 16:03:39 -06:00
Mauricio Siu
452793c8e5 Merge pull request #359 from Dokploy/canary
v0.7.0
2024-08-18 10:26:52 -06:00
Mauricio Siu
385fbf4af5 Merge pull request #355 from Dokploy/canary
v0.6.3
2024-08-16 22:26:35 -06:00
Mauricio Siu
3590f3bed2 Merge pull request #332 from Dokploy/canary
v0.6.2
2024-08-07 21:48:49 -06:00
Mauricio Siu
9b2fcaea31 Merge pull request #317 from Dokploy/canary
v0.6.1
2024-08-03 15:46:05 -06:00
Mauricio Siu
5abcc82215 Merge pull request #312 from Dokploy/canary
v0.6.0
2024-08-02 10:47:43 -06:00
Mauricio Siu
ee855452e3 Merge pull request #303 from Dokploy/canary
chore: add slash to version
2024-08-01 02:06:43 -06:00
Mauricio Siu
d000b526d3 Merge pull request #302 from Dokploy/canary
v0.5.1
2024-08-01 01:58:15 -06:00
Mauricio Siu
9bf88b90c3 Merge pull request #280 from Dokploy/canary
v0.5.0
2024-07-27 15:20:43 -06:00
Mauricio Siu
b1a48d4636 refactor: update job 2024-07-22 03:51:07 -06:00
Mauricio Siu
c34c4b244e Merge pull request #251 from Dokploy/canary
v0.4.0
2024-07-22 03:38:47 -06:00
Mauricio Siu
bb59a0cd3f Merge pull request #230 from Dokploy/canary
v0.3.3
2024-07-18 00:11:10 -06:00
Mauricio Siu
44e6a117dd Merge pull request #208 from Dokploy/canary
v0.3.2
2024-07-11 23:21:32 -06:00
Mauricio Siu
bfdc73f8d1 Merge pull request #197 from Dokploy/canary
v0.3.1
2024-07-06 12:01:07 -06:00
Mauricio Siu
64ada7020a Merge pull request #185 from Dokploy/canary
v0.3.0
2024-07-01 00:01:16 -06:00
Mauricio Siu
4706adc0c0 Merge pull request #174 from Dokploy/canary
v0.2.5
2024-06-29 13:29:39 -06:00
Mauricio Siu
e01d92d1d9 Merge pull request #161 from Dokploy/canary
v0.2.4
2024-06-23 19:40:45 -06:00
Mauricio Siu
fe22890311 Merge pull request #156 from Dokploy/canary
v0.2.3
2024-06-21 11:50:40 -06:00
Mauricio Siu
2b7c7632f4 Merge pull request #136 from Dokploy/canary
v0.2.2
2024-06-08 22:06:39 -06:00
Mauricio Siu
1b7244e841 Merge pull request #127 from Dokploy/canary
v0.2.1
2024-06-07 02:52:03 -06:00
61 changed files with 4927 additions and 540 deletions

View File

@@ -15,9 +15,7 @@ jobs:
name: Build and push AMD64 image
command: |
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
if [ "${CIRCLE_BRANCH}" == "139-multi-server-feature" ]; then
TAG="feature"
elif [ "${CIRCLE_BRANCH}" == "main" ]; then
if [ "${CIRCLE_BRANCH}" == "main" ]; then
TAG="latest"
else
TAG="canary"
@@ -40,9 +38,7 @@ jobs:
name: Build and push ARM64 image
command: |
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
if [ "${CIRCLE_BRANCH}" == "139-multi-server-feature" ]; then
TAG="feature"
elif [ "${CIRCLE_BRANCH}" == "main" ]; then
if [ "${CIRCLE_BRANCH}" == "main" ]; then
TAG="latest"
else
TAG="canary"
@@ -75,12 +71,6 @@ jobs:
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${VERSION}
elif [ "${CIRCLE_BRANCH}" == "139-multi-server-feature" ]; then
TAG="feature"
docker manifest create dokploy/dokploy:${TAG} \
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${TAG}
else
TAG="canary"
docker manifest create dokploy/dokploy:${TAG} \
@@ -98,14 +88,12 @@ workflows:
only:
- main
- canary
- 139-multi-server-feature
- build-arm64:
filters:
branches:
only:
- main
- canary
- 139-multi-server-feature
- combine-manifests:
requires:
- build-amd64
@@ -115,4 +103,3 @@ workflows:
only:
- main
- canary
- 139-multi-server-feature

View File

@@ -74,7 +74,7 @@ export function generateMetadata({
},
twitter: {
card: "summary_large_image",
creator: "@siumauricio",
creator: "@getdokploy",
title: page.data.title,
description: page.data.description,
images: [

View File

@@ -15,6 +15,8 @@ Configure the source of your code, the way your application is built, and also m
If you need to assign environment variables to your application, you can do so here.
In case you need to use a multiline variable, you can wrap it in double quotes just like this `'"here_is_my_private_key"'`.
## Monitoring
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.

View File

@@ -26,6 +26,8 @@ Actions like deploying, updating, and deleting your database, and stopping it.
If you need to assign environment variables to your application, you can do so here.
In case you need to use a multiline variable, you can wrap it in double quotes just like this `'"here_is_my_private_key"'`.
## Monitoring
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.

View File

@@ -1,22 +1,23 @@
---
title: 'Comparison'
description: 'A comparison of Dokploy, CapRover, Dokku, and Coolify'
title: "Comparison"
description: "A comparison of Dokploy, CapRover, Dokku, and Coolify"
---
Comparison of the following deployment tools:
| Feature | Dokploy | CapRover | Dokku | Coolify |
|-----------------------------------|---------------------------------------|--------------------------------------|--------------------------------------|--------------------------------------|
| **User Interface** | ✅ | ✅ | ❌ | ✅ |
| **Docker compose support** | | ❌ | ❌ | ✅ |
| **API/CLI** | ✅ | ✅ | ✅ | ✅ |
| **Multi node support** | ✅ | ✅ | ❌ | ✅ |
| **Traefik Integration** | ✅ | ✅ | Available via Plugins | ✅ |
| **User Permission Management** | ✅ | ❌ | ❌ | ✅ |
| **Advanced User Permission Management** | ✅ | ❌ | ❌ | ❌ |
| **Terminal Access Built In** | ✅ | ❌ | ❌ | ✅ |
| **Database Support** | ✅ | ✅ | ❌ | ✅ |
| **Monitoring** | ✅ | ✅ | ❌ | ❌ |
| **Backups** | ✅ | Available via Plugins | Available via Plugins | ✅ |
| **Open Source** | ✅ | ✅ | ✅ | ✅ |
| **Cloud/Paid Version** | ❌ | ✅ | ❌ | ✅ |
| Feature | Dokploy | CapRover | Dokku | Coolify |
| --------------------------------------- | ------- | --------------------- | --------------------- | ------- |
| **User Interface** | ✅ | ✅ | ❌ | ✅ |
| **Docker compose support** || ❌ | ❌ | ✅ |
| **API/CLI** | ✅ | ✅ | ✅ | ✅ |
| **Multi node support** | ✅ | ✅ | ❌ | ✅ |
| **Traefik Integration** | ✅ | ✅ | Available via Plugins | ✅ |
| **User Permission Management** | ✅ | ❌ | ❌ | ✅ |
| **Advanced User Permission Management** | ✅ | ❌ | ❌ | ❌ |
| **Terminal Access Built In** | ✅ | ❌ | ❌ | ✅ |
| **Database Support** | ✅ | ✅ | ❌ | ✅ |
| **Monitoring** | ✅ | ✅ | ❌ | ❌ |
| **Backups** | ✅ | Available via Plugins | Available via Plugins | ✅ |
| **Open Source** | ✅ | ✅ | ✅ | ✅ |
| **Multi Server Support** | ✅ | ❌ | ❌ | ✅ |
| **Cloud/Paid Version** | ❌ | ✅ | ✅ | ✅ |

View File

@@ -64,6 +64,9 @@
"docker/overview",
"---Monitoring---",
"monitoring/overview",
"---Multi Server---",
"multi-server/overview",
"multi-server/example",
"---Cluster---",
"cluster/overview",
"---Deployments---",

View File

@@ -0,0 +1,117 @@
---
title: Example
description: "Example to setup a remote server and deploy application in a VPS."
---
import { Callout } from "fumadocs-ui/components/callout";
Multi server allows you to deploy your apps remotely to different servers without needing to build and run them where the Dokploy UI is installed.
## Requirements
1. To install Dokploy UI, follow the [installation guide](en/docs/core/get-started/installation).
2. Create an SSH key by going to `/dashboard/settings/ssh-keys` and add a new key. Be sure to copy the public key.
<ImageZoom
src="/assets/ssh-keys.png"
alt="Architecture Diagram"
width={1000}
height={600}
className="rounded-lg"
/>
3. Decide which remote server to deploy your apps on. We recommend these reliable providers:
- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% off with this [referral link](https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97).
- [DigitalOcean](https://www.digitalocean.com/pricing/droplets#basic-droplets) Get $200 credits for free with this [referral link](https://m.do.co/c/db24efd43f35).
- [Hetzner](https://www.hetzner.com/cloud/) Get €20 credits with this [referral link](https://hetzner.cloud/?ref=vou4fhxJ1W2D).
- [Linode](https://www.linode.com/es/pricing/#compute-shared).
- [Vultr](https://www.vultr.com/pricing/#cloud-compute).
- [Scaleway](https://www.scaleway.com/en/pricing/?tags=baremetal,available).
- [Google Cloud](https://cloud.google.com/).
- [AWS](https://aws.amazon.com/ec2/pricing/).
4. When creating the server, it should ask for SSH keys. Ideally, use your computer's public key and the key you generated in the previous step. Here's how to add the public key in Hostinger:
<ImageZoom
src="/assets/hostinger-add-sshkey.png"
alt="Adding SSH key"
width={1000}
height={600}
className="rounded-lg"
/>
<Callout>The steps are similar across other providers.</Callout>
5. Copy the servers IP address and ensure you know the username (often `root`). Fill in all fields and click `Create`.
<ImageZoom
src="/assets/multi-server-add-server.png"
alt="Add server"
width={1000}
height={600}
className="rounded-lg"
/>
6. To test connectivity, open the server dropdown and click `Enter Terminal`. If everything is correct, you should be able to interact with the remote server.
7. Click `Setup Server` to proceed. There are two tabs: SSH Keys and Deployments. This guide explains the easy way, but you can follow the manual process via the Dokploy UI if you prefer.
<ImageZoom
src="/assets/multi-server-setup-2.png"
alt="Setup process"
width={1000}
height={600}
className="rounded-lg"
/>
8. Click `Deployments`, then `Setup Server`. If everything is correct, you should see output similar to this:
<ImageZoom
src="/assets/multi-server-setup-3.png"
alt="Server setup output"
width={1000}
height={600}
className="rounded-lg"
/>
<Callout>
You only need to run this setup once. If Dokploy updates later, check the
release notes to see if rerunning this command is required.
</Callout>
9. You're ready to deploy your apps! Let's test it out:
<ImageZoom
src="/assets/multi-server-add-app.png"
alt="Add app"
width={1000}
height={600}
className="rounded-lg"
/>
10. To check which server an app belongs to, youll see the server name at the top. If no server is selected, it defaults to `Dokploy Server`. Click `Deploy` to start building your app on the remote server. You can check the `Logs` tab to see the build process. For this example, well use a test repo:
Repo: `https://github.com/Dokploy/examples.git`
Branch: `main`
Build Path: `/astro`
<ImageZoom
src="/assets/multi-server-setup-app.png"
alt="App setup"
width={1000}
height={600}
className="rounded-lg"
/>
11. Once the build is done, go to `Domains` and create a free domain. Just click `Create` and youre good to go! 🎊
{" "}
<ImageZoom
src="/assets/multi-server-finish.png"
alt="Finished setup"
width={1000}
height={600}
className="rounded-lg"
/>

View File

@@ -0,0 +1,29 @@
---
title: Overview
description: "Deploy your apps to multiple servers remotely."
---
import { Callout } from "fumadocs-ui/components/callout";
Multi server allows you to deploy your apps remotely to different servers without needing to build and run them where the Dokploy UI is installed.
To use the multi-server feature, you need to have Dokploy UI installed either locally or on a remote server. We recommend using a remote server for better connectivity, security, and isolation, for remote instances we install only a traefik instance.
If you plan to only deploy apps to remote servers and use Dokploy UI for managing deployments, Dokploy will use around 250 MB of RAM and minimal CPU, so a low-resource server should be sufficient.
All the features we have documented previously are supported by Dokploy Multi Server. The only feature not supported is remote server monitoring, due to performance reasons. However, all functionalities should work the same as when deploying on the same server where Dokploy UI is installed.
## Features
1. **Enter the terminal**: Allows you to access the terminal of the remote server.
2. **Setup Server**: Allows you to configure the remote server.
- **SSH Keys**: Steps to add SSH keys to the remote server.
- **Deployments**: Steps to configure the remote server for deploying applications.
3. **Edit Server**: Allows you to modify the remote server's details, such as SSH key, name, description, IP, etc.
4. **View Actions**: Lets you perform actions like managing the Traefik instance, storage, and activating Docker cleanup.
5. **Show Traefik File System**: Displays the contents of the remote server's directory.
6. **Show Docker Containers**: Shows the Docker containers running on the remote server.
<Callout>
Remote server monitoring is not supported due to performance reasons.
</Callout>

View File

@@ -3,4 +3,90 @@ title: Overview
description: Solve the most common problems that occur when using Dokploy.
---
WIP
## Applications Domain Not Working?
You see the deployment succeeded, and logs are running, but the domain isn't working? Here's what to check:
1. **Correct Port Mapping**: Ensure the domain is using the correct port for your application. For example, if you're using Next.js, the port should be `3000`, or for Laravel, it should be `8000`. If you change the app port, update the domain to reflect that.
2. **Avoid Using `Ports` in Advanced Settings**: Generally, there's no need to use the `Ports` feature unless you want to access your app via `IP:port`. Leaving this feature enabled may interfere with your domain.
3. **Let's Encrypt Certificates**: It's crucial to point the domain to your servers IP **before** adding it in Dokploy. If the domain is added first, the certificate wont be generated, and you may need to recreate the domain or restart Traefik.
4. **Listen on 0.0.0.0, Not 127.0.0.1**: If your app is bound to `127.0.0.1` (which is common in Vite apps), switch it to `0.0.0.0` to allow external access.
## Logs and Monitoring Not Working After Changing Application Placement?
This is expected behavior. If the application is running on a different node (worker), the UI wont have access to logs or monitoring, as they're not on the same node.
## Mounts Are Causing My Application Not to Run?
Docker Swarm won't run your application if there are invalid mounts, even if the deployment shows as successful. Double-check your mounts to ensure they are valid.
## Volumes in Docker Compose Not Working?
For Docker Compose, all file mounts defined in the `volumes` section will be stored in the `files` folder. This is the default directory structure:
## I added a volume to my docker compose, but is not finding the volume?
For docker compose all the file mounts you've created in the volumes section will be stored to files folder, this is the default structure of the docker compose.
```
/application-name
/code
/files
```
So instead of using this invalid way to mount a volume:
```yaml
volumes:
- "/folder:/path/in/container" ❌
```
You should use this format:
```yaml
volumes:
- "../files/my-database:/var/lib/mysql" ✅
- "../files/my-configs:/etc/my-app/config" ✅
```
## Logs Not Loading When Deploying to a Remote Server?
There are a few potential reasons for this:
1. **Slow Server:**: If the server is too slow, it may struggle to handle concurrent requests, leading to SSL handshake errors.
2. **Insufficient Disk Space:** If the server doesn't have enough disk space, the logs may not load.
## Docker Compose Domain Not Working?
When adding a domain in your Docker Compose file, its not necessary to expose the ports directly. Simply specify the port where your app is running. Exposing the ports can lead to conflicts with other applications or ports.
Example of what not to do:
```yaml
services:
app:
image: dokploy/dokploy:latest
ports:
- 3000:3000
```
Recommended approach:
```yaml
services:
app:
image: dokploy/dokploy:latest
ports:
- 3000
- 80
```
Then, when creating the domain in Dokploy, specify the service name and port, like this:
```yaml
domain: my-app.com
serviceName: app
port: 3000
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View File

@@ -17,7 +17,7 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Input, NumberInput } from "@/components/ui/input";
import {
Select,
SelectContent,
@@ -125,28 +125,14 @@ export const UpdatePort = ({ portId }: Props) => {
<FormItem>
<FormLabel>Published Port</FormLabel>
<FormControl>
<Input
placeholder="1-65535"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(0);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
<NumberInput placeholder="1-65535" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="targetPort"
@@ -154,22 +140,7 @@ export const UpdatePort = ({ portId }: Props) => {
<FormItem>
<FormLabel>Target Port</FormLabel>
<FormControl>
<Input
placeholder="1-65535"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(0);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
<Input placeholder="1-65535" {...field} />
</FormControl>
<FormMessage />

View File

@@ -19,6 +19,15 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -36,6 +45,36 @@ const AddRedirectchema = z.object({
type AddRedirect = z.infer<typeof AddRedirectchema>;
// Default presets
const redirectPresets = [
// {
// label: "Allow www & non-www.",
// redirect: {
// regex: "",
// permanent: false,
// replacement: "",
// },
// },
{
id: "to-www",
label: "Redirect to www",
redirect: {
regex: "^https?://(?:www.)?(.+)",
permanent: true,
replacement: "https://www.$${1}",
},
},
{
id: "to-non-www",
label: "Redirect to non-www",
redirect: {
regex: "^https?://www.(.+)",
permanent: true,
replacement: "https://$${1}",
},
},
];
interface Props {
applicationId: string;
children?: React.ReactNode;
@@ -43,9 +82,10 @@ interface Props {
export const AddRedirect = ({
applicationId,
children = <PlusIcon className="h-4 w-4" />,
children = <PlusIcon className="w-4 h-4" />,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [presetSelected, setPresetSelected] = useState("");
const utils = api.useUtils();
const { mutateAsync, isLoading, error, isError } =
@@ -81,19 +121,36 @@ export const AddRedirect = ({
await utils.application.readTraefikConfig.invalidate({
applicationId,
});
setIsOpen(false);
onDialogToggle(false);
})
.catch(() => {
toast.error("Error to create the redirect");
});
};
const onDialogToggle = (open: boolean) => {
setIsOpen(open);
// commented for the moment because not reseting the form if accidentally closed the dialog can be considered as a feature instead of a bug
// setPresetSelected("");
// form.reset();
};
const onPresetSelect = (presetId: string) => {
const redirectPreset = redirectPresets.find(
(preset) => preset.id === presetId,
)?.redirect;
if (!redirectPreset) return;
const { regex, permanent, replacement } = redirectPreset;
form.reset({ regex, permanent, replacement }, { keepDefaultValues: true });
setPresetSelected(presetId);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
<DialogTrigger asChild>
<Button>{children}</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Redirects</DialogTitle>
<DialogDescription>
@@ -102,6 +159,24 @@ export const AddRedirect = ({
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="md:col-span-2">
<Label>Presets</Label>
<Select onValueChange={onPresetSelect} value={presetSelected}>
<SelectTrigger>
<SelectValue placeholder="No preset selected" />
</SelectTrigger>
<SelectContent>
{redirectPresets.map((preset) => (
<SelectItem key={preset.label} value={preset.id}>
{preset.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Separator />
<Form {...form}>
<form
id="hook-form-add-redirect"
@@ -142,7 +217,7 @@ export const AddRedirect = ({
control={form.control}
name="permanent"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Permanent</FormLabel>
<FormDescription>

View File

@@ -18,7 +18,7 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Input, NumberInput } from "@/components/ui/input";
import {
Select,
SelectContent,
@@ -140,7 +140,7 @@ export const AddDomain = ({
<DialogTrigger className="" asChild>
{children}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Domain</DialogTitle>
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
@@ -228,19 +228,36 @@ export const AddDomain = ({
<FormItem>
<FormLabel>Container Port</FormLabel>
<FormControl>
<Input
placeholder={"3000"}
{...field}
onChange={(e) => {
field.onChange(Number.parseInt(e.target.value));
}}
/>
<NumberInput placeholder={"3000"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="https"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>HTTPS</FormLabel>
<FormDescription>
Automatically provision SSL Certificate.
</FormDescription>
<FormMessage />
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{form.getValues().https && (
<FormField
control={form.control}
@@ -270,28 +287,6 @@ export const AddDomain = ({
)}
/>
)}
<FormField
control={form.control}
name="https"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>HTTPS</FormLabel>
<FormDescription>
Automatically provision SSL Certificate.
</FormDescription>
<FormMessage />
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</div>
</form>

View File

@@ -18,7 +18,7 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Input, NumberInput } from "@/components/ui/input";
import {
Select,
SelectContent,
@@ -161,7 +161,7 @@ export const AddDomainCompose = ({
<DialogTrigger className="" asChild>
{children}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Domain</DialogTitle>
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
@@ -190,7 +190,7 @@ export const AddDomainCompose = ({
{errorServices?.message}
</AlertBlock>
)}
<div className="flex flex-row gap-4 w-full items-end">
<div className="flex flex-row items-end w-full gap-4">
<FormField
control={form.control}
name="serviceName"
@@ -364,19 +364,36 @@ export const AddDomainCompose = ({
<FormItem>
<FormLabel>Container Port</FormLabel>
<FormControl>
<Input
placeholder={"3000"}
{...field}
onChange={(e) => {
field.onChange(Number.parseInt(e.target.value));
}}
/>
<NumberInput placeholder={"3000"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="https"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>HTTPS</FormLabel>
<FormDescription>
Automatically provision SSL Certificate.
</FormDescription>
<FormMessage />
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{https && (
<FormField
control={form.control}
@@ -406,28 +423,6 @@ export const AddDomainCompose = ({
)}
/>
)}
<FormField
control={form.control}
name="https"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>HTTPS</FormLabel>
<FormDescription>
Automatically provision SSL Certificate.
</FormDescription>
<FormMessage />
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</div>
</form>

View File

@@ -119,7 +119,6 @@ export const ComposeActions = ({ composeId }: Props) => {
</DropdownMenuContent>
</DropdownMenu>
)}
{data?.server?.name}
</div>
);
};

View File

@@ -48,6 +48,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
@@ -79,7 +80,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
};
setConnectionUrl(buildConnectionUrl());
@@ -90,7 +91,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
form,
data?.databaseName,
data?.databaseUser,
ip,
getIp,
]);
return (
<>

View File

@@ -48,7 +48,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
@@ -80,7 +80,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}`;
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
};
setConnectionUrl(buildConnectionUrl());
@@ -90,7 +90,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
data?.databasePassword,
form,
data?.databaseUser,
ip,
getIp,
]);
return (

View File

@@ -48,7 +48,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
@@ -80,7 +80,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
};
setConnectionUrl(buildConnectionUrl());
@@ -91,7 +91,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
data?.databaseName,
data?.databaseUser,
form,
ip,
getIp,
]);
return (
<>

View File

@@ -48,6 +48,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
const { mutateAsync, isLoading } =
api.postgres.saveExternalPort.useMutation();
const getIp = data?.server?.ipAddress || ip;
const [connectionUrl, setConnectionUrl] = useState("");
const form = useForm<DockerProvider>({
@@ -79,10 +80,9 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
useEffect(() => {
const buildConnectionUrl = () => {
const hostname = window.location.hostname;
const port = form.watch("externalPort") || data?.externalPort;
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
};
setConnectionUrl(buildConnectionUrl());
@@ -92,7 +92,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
data?.databasePassword,
form,
data?.databaseName,
ip,
getIp,
]);
return (

View File

@@ -145,7 +145,10 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
form.setValue("appName", `${slug}-${val}`);
form.setValue(
"appName",
`${slug}-${val.toLowerCase().replaceAll(" ", "-")}`,
);
field.onChange(val);
}}
/>

View File

@@ -39,7 +39,7 @@ import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -71,6 +71,7 @@ interface Props {
export const AddCompose = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync, isLoading, error, isError } =
@@ -101,6 +102,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
})
.then(async () => {
toast.success("Compose Created");
setVisible(false);
await utils.project.one.invalidate({
projectId,
});
@@ -111,7 +113,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
};
return (
<Dialog>
<Dialog open={visible} onOpenChange={setVisible}>
<DialogTrigger className="w-full">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
@@ -149,7 +151,10 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
form.setValue("appName", `${slug}-${val}`);
form.setValue(
"appName",
`${slug}-${val.toLowerCase()}`,
);
field.onChange(val);
}}
/>

View File

@@ -361,7 +361,10 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
form.setValue("appName", `${slug}-${val}`);
form.setValue(
"appName",
`${slug}-${val.toLowerCase()}`,
);
field.onChange(val);
}}
/>

View File

@@ -48,6 +48,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
const { data, refetch } = api.redis.one.useQuery({ redisId });
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({
defaultValues: {},
@@ -81,11 +82,11 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
const hostname = window.location.hostname;
const port = form.watch("externalPort") || data?.externalPort;
return `redis://default:${data?.databasePassword}@${ip}:${port}`;
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
};
setConnectionUrl(buildConnectionUrl());
}, [data?.appName, data?.externalPort, data?.databasePassword, form, ip]);
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
return (
<>
<div className="flex w-full flex-col gap-5 ">

View File

@@ -17,10 +17,18 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Container } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -36,10 +44,9 @@ const AddRegistrySchema = z.object({
password: z.string().min(1, {
message: "Password is required",
}),
registryUrl: z.string().min(1, {
message: "Registry URL is required",
}),
registryUrl: z.string(),
imagePrefix: z.string(),
serverId: z.string().optional(),
});
type AddRegistry = z.infer<typeof AddRegistrySchema>;
@@ -48,9 +55,9 @@ export const AddRegistry = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.registry.create.useMutation();
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync: testRegistry, isLoading } =
api.registry.testRegistry.useMutation();
const router = useRouter();
const form = useForm<AddRegistry>({
defaultValues: {
username: "",
@@ -58,6 +65,7 @@ export const AddRegistry = () => {
registryUrl: "",
imagePrefix: "",
registryName: "",
serverId: "",
},
resolver: zodResolver(AddRegistrySchema),
});
@@ -67,6 +75,7 @@ export const AddRegistry = () => {
const registryUrl = form.watch("registryUrl");
const registryName = form.watch("registryName");
const imagePrefix = form.watch("imagePrefix");
const serverId = form.watch("serverId");
useEffect(() => {
form.reset({
@@ -74,6 +83,7 @@ export const AddRegistry = () => {
password: "",
registryUrl: "",
imagePrefix: "",
serverId: "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
@@ -85,6 +95,7 @@ export const AddRegistry = () => {
registryUrl: data.registryUrl,
registryType: "cloud",
imagePrefix: data.imagePrefix,
serverId: data.serverId,
})
.then(async (data) => {
await utils.registry.all.invalidate();
@@ -211,34 +222,77 @@ export const AddRegistry = () => {
)}
/>
</div>
<DialogFooter className="flex flex-row w-full sm:justify-between gap-4 flex-wrap">
<Button
type="button"
variant={"secondary"}
isLoading={isLoading}
onClick={async () => {
await testRegistry({
username: username,
password: password,
registryUrl: registryUrl,
registryName: registryName,
registryType: "cloud",
imagePrefix: imagePrefix,
})
.then((data) => {
if (data) {
toast.success("Registry Tested Successfully");
} else {
toast.error("Registry Test Failed");
}
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
<div className="flex flex-col gap-4 border p-2 rounded-lg">
<span className="text-sm text-muted-foreground">
Select a server to test the registry. If you don't have a
server choose the default one.
</span>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Server (Optional)</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Servers</SelectLabel>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant={"secondary"}
isLoading={isLoading}
onClick={async () => {
await testRegistry({
username: username,
password: password,
registryUrl: registryUrl,
registryName: registryName,
registryType: "cloud",
imagePrefix: imagePrefix,
serverId: serverId,
})
.catch(() => {
toast.error("Error to test the registry");
});
}}
>
Test Registry
</Button>
.then((data) => {
if (data) {
toast.success("Registry Tested Successfully");
} else {
toast.error("Registry Test Failed");
}
})
.catch(() => {
toast.error("Error to test the registry");
});
}}
>
Test Registry
</Button>
</div>
<Button isLoading={form.formState.isSubmitting} type="submit">
Create
</Button>

View File

@@ -17,6 +17,15 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -34,10 +43,9 @@ const updateRegistry = z.object({
message: "Username is required",
}),
password: z.string(),
registryUrl: z.string().min(1, {
message: "Registry URL is required",
}),
registryUrl: z.string(),
imagePrefix: z.string(),
serverId: z.string().optional(),
});
type UpdateRegistry = z.infer<typeof updateRegistry>;
@@ -48,6 +56,8 @@ interface Props {
export const UpdateDockerRegistry = ({ registryId }: Props) => {
const utils = api.useUtils();
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync: testRegistry, isLoading } =
api.registry.testRegistry.useMutation();
const { data, refetch } = api.registry.one.useQuery(
@@ -69,15 +79,19 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
username: "",
password: "",
registryUrl: "",
serverId: "",
},
resolver: zodResolver(updateRegistry),
});
console.log(form.formState.errors);
const password = form.watch("password");
const username = form.watch("username");
const registryUrl = form.watch("registryUrl");
const registryName = form.watch("registryName");
const imagePrefix = form.watch("imagePrefix");
const serverId = form.watch("serverId");
useEffect(() => {
if (data) {
@@ -87,6 +101,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
username: data.username || "",
password: "",
registryUrl: data.registryUrl || "",
serverId: "",
});
}
}, [form, form.reset, data]);
@@ -99,6 +114,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
username: data.username,
registryUrl: data.registryUrl,
imagePrefix: data.imagePrefix,
serverId: data.serverId,
})
.then(async (data) => {
toast.success("Registry Updated");
@@ -224,13 +240,47 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
</div>
</form>
<DialogFooter
className={cn(
isCloud ? "sm:justify-between " : "",
"flex flex-row w-full gap-4 flex-wrap",
)}
>
{isCloud && (
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
<div className="flex flex-col gap-4 border p-2 rounded-lg">
<span className="text-sm text-muted-foreground">
Select a server to test the registry. If you don't have a server
choose the default one.
</span>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Server (Optional)</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Servers</SelectLabel>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant={"secondary"}
@@ -243,6 +293,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
registryName: registryName,
registryType: "cloud",
imagePrefix: imagePrefix,
serverId: serverId,
})
.then((data) => {
if (data) {
@@ -258,12 +309,12 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
>
Test Registry
</Button>
)}
</div>
<Button
isLoading={form.formState.isSubmitting}
form="hook-form"
type="submit"
form="hook-form"
>
Update
</Button>

View File

@@ -212,7 +212,21 @@ export const AddServer = () => {
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input placeholder="22" {...field} />
<Input
placeholder="22"
{...field}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(0);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />

View File

@@ -1,3 +1,5 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip";
@@ -17,22 +19,20 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import {
CopyIcon,
ExternalLinkIcon,
RocketIcon,
ServerIcon,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { ShowDeployment } from "../../application/deployments/show-deployment";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { CodeEditor } from "@/components/shared/code-editor";
import copy from "copy-to-clipboard";
import Link from "next/link";
import { Separator } from "@/components/ui/separator";
import { AlertBlock } from "@/components/shared/alert-block";
interface Props {
serverId: string;
@@ -59,8 +59,6 @@ export const SetupServer = ({ serverId }: Props) => {
const { mutateAsync, isLoading } = api.server.setup.useMutation();
console.log(server?.sshKey);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
@@ -181,7 +179,9 @@ export const SetupServer = ({ serverId }: Props) => {
type="button"
className="absolute right-2 top-2"
onClick={() => {
copy(server?.sshKey?.publicKey || "");
copy(
`echo "${server?.sshKey?.publicKey}" >> ~/.ssh/authorized_keys`,
);
toast.success("Copied to clipboard");
}}
>

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -30,7 +31,6 @@ import { SetupServer } from "./setup-server";
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { UpdateServer } from "./update-server";
import { AlertBlock } from "@/components/shared/alert-block";
export const ShowServers = () => {
const { data, refetch } = api.server.all.useQuery();

View File

@@ -228,7 +228,21 @@ export const UpdateServer = ({ serverId }: Props) => {
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input placeholder="22" {...field} />
<Input
placeholder="22"
{...field}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(0);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />

View File

@@ -31,4 +31,39 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
);
Input.displayName = "Input";
export { Input };
const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
({ className, errorMessage, ...props }, ref) => {
return (
<Input
type="text"
className={cn("text-left", className)}
ref={ref}
{...props}
value={props.value === undefined ? undefined : String(props.value)}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
props.onChange?.(e);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
const syntheticEvent = {
...e,
target: {
...e.target,
value: number,
},
};
props.onChange?.(
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
);
}
}
}}
/>
);
},
);
NumberInput.displayName = "NumberInput";
export { Input, NumberInput };

View File

@@ -0,0 +1 @@
ALTER TABLE "registry" ALTER COLUMN "registryUrl" SET DEFAULT '';

File diff suppressed because it is too large Load Diff

View File

@@ -267,6 +267,13 @@
"when": 1726988289562,
"tag": "0037_legal_namor",
"breakpoints": true
},
{
"idx": 38,
"version": "6",
"when": 1727942090102,
"tag": "0038_rapid_landau",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.8.3",
"version": "v0.9.4",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -68,7 +68,7 @@ export const destinationRouter = createTRPCRouter({
const destination = await findDestinationById(input.destinationId);
return destination;
}),
all: adminProcedure.query(async () => {
all: protectedProcedure.query(async () => {
return await db.query.destinations.findMany({});
}),
remove: adminProcedure

View File

@@ -7,7 +7,7 @@ import {
apiUpdateRegistry,
} from "@/server/db/schema";
import { initializeRegistry } from "@/server/setup/registry-setup";
import { execAsync } from "@/server/utils/process/execAsync";
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
import { manageRegistry } from "@/server/utils/traefik/registry";
import { TRPCError } from "@trpc/server";
import {
@@ -58,7 +58,13 @@ export const registryRouter = createTRPCRouter({
.mutation(async ({ input }) => {
try {
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
await execAsync(loginCommand);
if (input.serverId && input.serverId !== "none") {
await execAsyncRemote(input.serverId, loginCommand);
} else {
await execAsync(loginCommand);
}
return true;
} catch (error) {
console.log("Error Registry:", error);
@@ -78,6 +84,7 @@ export const registryRouter = createTRPCRouter({
? input.registryUrl
: "dokploy-registry.docker.localhost",
imagePrefix: null,
serverId: undefined,
});
await manageRegistry(selfHostedRegistry);
@@ -86,3 +93,17 @@ export const registryRouter = createTRPCRouter({
return selfHostedRegistry;
}),
});
const shellEscape = (str: string) => {
const ret = [];
let s = str;
if (/[^A-Za-z0-9_\/:=-]/.test(s)) {
s = `'${s.replace(/'/g, "'\\''")}'`;
s = s
.replace(/^(?:'')+/g, "") // unduplicate single-quote at the beginning
.replace(/\\'''/g, "\\'"); // remove non-escaped single-quote if there are enclosed between 2 escaped
}
ret.push(s);
return ret.join(" ");
};

View File

@@ -8,11 +8,9 @@ import {
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { isAfter } from "date-fns";
import { eq } from "drizzle-orm";
export type Admin = typeof admins.$inferSelect;
export const createInvitation = async (
input: typeof apiCreateUserInvitation._type,
) => {

View File

@@ -2,7 +2,7 @@ import { db } from "@/server/db";
import { type apiCreateRegistry, registry } from "@/server/db/schema";
import { initializeRegistry } from "@/server/setup/registry-setup";
import { removeService } from "@/server/utils/docker/utils";
import { execAsync } from "@/server/utils/process/execAsync";
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
import {
manageRegistry,
removeSelfHostedRegistry,
@@ -32,9 +32,10 @@ export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
message: "Error input: Inserting registry",
});
}
if (newRegistry.registryType === "cloud") {
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
if (input.serverId && input.serverId !== "none") {
await execAsyncRemote(input.serverId, loginCommand);
} else if (newRegistry.registryType === "cloud") {
await execAsync(loginCommand);
}
@@ -76,7 +77,7 @@ export const removeRegistry = async (registryId: string) => {
export const updateRegistry = async (
registryId: string,
registryData: Partial<Registry>,
registryData: Partial<Registry> & { serverId?: string | null },
) => {
try {
const response = await db
@@ -92,6 +93,13 @@ export const updateRegistry = async (
await manageRegistry(response);
await initializeRegistry(response.username, response.password);
}
const loginCommand = `echo ${response?.password} | docker login ${response?.registryUrl} --username ${response?.username} --password-stdin`;
if (registryData?.serverId && registryData?.serverId !== "none") {
await execAsyncRemote(registryData.serverId, loginCommand);
} else if (response?.registryType === "cloud") {
await execAsync(loginCommand);
}
return response;
} catch (error) {

View File

@@ -1,6 +1,8 @@
import type { IncomingMessage } from "node:http";
import { TimeSpan } from "lucia";
import { Lucia } from "lucia/dist/core.js";
import { findAdminByAuthId } from "../api/services/admin";
import { findUserByAuthId } from "../api/services/user";
import { type ReturnValidateToken, adapter } from "./auth";
export const luciaToken = new Lucia(adapter, {
@@ -31,6 +33,16 @@ export const validateBearerToken = async (
};
}
const result = await luciaToken.validateSession(sessionId);
if (result.user) {
if (result.user?.rol === "admin") {
const admin = await findAdminByAuthId(result.user.id);
result.user.adminId = admin.adminId;
} else if (result.user?.rol === "user") {
const userResult = await findUserByAuthId(result.user.id);
result.user.adminId = userResult.adminId;
}
}
return {
session: result.session,
...((result.user && {

View File

@@ -23,7 +23,7 @@ export const registry = pgTable("registry", {
imagePrefix: text("imagePrefix"),
username: text("username").notNull(),
password: text("password").notNull(),
registryUrl: text("registryUrl").notNull(),
registryUrl: text("registryUrl").notNull().default(""),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
@@ -45,7 +45,7 @@ const createSchema = createInsertSchema(registry, {
registryName: z.string().min(1),
username: z.string().min(1),
password: z.string().min(1),
registryUrl: z.string().min(1),
registryUrl: z.string(),
adminId: z.string().min(1),
registryId: z.string().min(1),
registryType: z.enum(["selfHosted", "cloud"]),
@@ -62,7 +62,10 @@ export const apiCreateRegistry = createSchema
registryType: z.enum(["selfHosted", "cloud"]),
imagePrefix: z.string().nullable().optional(),
})
.required();
.required()
.extend({
serverId: z.string().optional(),
});
export const apiTestRegistry = createSchema.pick({}).extend({
registryName: z.string().min(1),
@@ -71,6 +74,7 @@ export const apiTestRegistry = createSchema.pick({}).extend({
registryUrl: z.string(),
registryType: z.enum(["selfHosted", "cloud"]),
imagePrefix: z.string().nullable().optional(),
serverId: z.string().optional(),
});
export const apiRemoveRegistry = createSchema
@@ -87,6 +91,7 @@ export const apiFindOneRegistry = createSchema
export const apiUpdateRegistry = createSchema.partial().extend({
registryId: z.string().min(1),
serverId: z.string().optional(),
});
export const apiEnableSelfHostedRegistry = createSchema

View File

@@ -65,7 +65,6 @@ const installRequirements = async (serverId: string, logPath: string) => {
return new Promise<void>((resolve, reject) => {
client
.once("ready", () => {
console.log("Client :: ready");
const bashCommand = `
${validatePorts()}

View File

@@ -1,4 +1,4 @@
import type { WriteStream } from "node:fs";
import { type WriteStream, existsSync, mkdirSync } from "node:fs";
import path from "node:path";
import { buildStatic, getStaticCommand } from "@/server/utils/builders/static";
import { nanoid } from "nanoid";
@@ -42,7 +42,6 @@ export const buildNixpacks = async (
and copy the artifacts on the host filesystem.
Then, remove the container and create a static build.
*/
if (publishDirectory) {
await spawnAsync(
"docker",
@@ -50,12 +49,22 @@ export const buildNixpacks = async (
writeToStream,
);
const localPath = path.join(buildAppDirectory, publishDirectory);
if (!existsSync(path.dirname(localPath))) {
mkdirSync(path.dirname(localPath), { recursive: true });
}
// https://docs.docker.com/reference/cli/docker/container/cp/
const isDirectory =
publishDirectory.endsWith("/") || !path.extname(publishDirectory);
await spawnAsync(
"docker",
[
"cp",
`${buildContainerId}:/app/${publishDirectory}`,
path.join(buildAppDirectory, publishDirectory),
`${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""}`,
localPath,
],
writeToStream,
);
@@ -92,7 +101,6 @@ export const getNixpacksCommand = (
/* No need for any start command, since we'll use nginx later on */
args.push("--no-error-without-start");
}
console.log("args", args);
const command = `nixpacks ${args.join(" ")}`;
let bashCommand = `
echo "Starting nixpacks build..." >> ${logPath};
@@ -109,9 +117,14 @@ echo "✅ Nixpacks build completed." >> ${logPath};
Then, remove the container and create a static build.
*/
if (publishDirectory) {
const localPath = path.join(buildAppDirectory, publishDirectory);
const isDirectory =
publishDirectory.endsWith("/") || !path.extname(publishDirectory);
bashCommand += `
docker create --name ${buildContainerId} ${appName}
docker cp ${buildContainerId}:/app/${publishDirectory} ${path.join(buildAppDirectory, publishDirectory)} >> ${logPath} 2>> ${logPath} || {
mkdir -p ${localPath}
docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} >> ${logPath} 2>> ${logPath} || {
docker rm ${buildContainerId}
echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" >> ${logPath};
exit 1;

View File

@@ -23,14 +23,10 @@ export const execAsyncRemote = async (
sleep(1000);
conn
.once("ready", () => {
console.log("Client :: ready");
conn.exec(command, (err, stream) => {
if (err) throw err;
stream
.on("close", (code: number, signal: string) => {
console.log(
`Stream :: close :: code: ${code}, signal: ${signal}`,
);
conn.end();
if (code === 0) {
resolve({ stdout, stderr });

View File

@@ -147,15 +147,15 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
const bitbucketProvider = await findBitbucketById(bitbucketId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
try {
await execAsyncRemote(
serverId,
`git clone --branch ${bitbucketBranch} --depth 1 ${cloneUrl} ${outputPath}`,
);
const command = `
rm -rf ${outputPath};
git clone --branch ${bitbucketBranch} --depth 1 ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {
throw error;
}

View File

@@ -271,13 +271,13 @@ export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
await recreateDirectory(outputPath);
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try {
await execAsyncRemote(
serverId,
`git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}`,
);
const command = `
rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {
throw error;
}

View File

@@ -390,14 +390,14 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
await refreshGitlabToken(gitlabId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
try {
await execAsyncRemote(
serverId,
`git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}`,
);
const command = `
rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {
throw error;
}

View File

@@ -70,6 +70,7 @@ export const createComposeFileRawRemote = async (compose: Compose) => {
try {
const encodedContent = encodeBase64(composeFile);
const command = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
echo "${encodedContent}" | base64 -d > "${filePath}";
`;

View File

@@ -72,6 +72,8 @@ export const setupTerminalWebSocketServer = (
"StrictHostKeyChecking=no",
"-i",
privateKey,
"-p",
`${server.port}`,
`${server.username}@${server.ipAddress}`,
];
const ptyProcess = spawn("ssh", sshCommand.slice(1), {

View File

@@ -33,7 +33,7 @@ export function Footer() {
<div className="flex flex-col items-center border-t border-slate-400/10 py-10 sm:flex-row-reverse sm:justify-between">
<div className="flex gap-x-6">
<Link
href="https://twitter.com/Siumauricio"
href="https://x.com/getdokploy"
className="group"
aria-label="Dokploy on Twitter"
>

View File

@@ -1,114 +1,133 @@
#!/bin/bash
install_dokploy() {
if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root" >&2
exit 1
fi
if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root" >&2
exit 1
fi
# check if is Mac OS
if [ "$(uname)" = "Darwin" ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if is Mac OS
if [ "$(uname)" = "Darwin" ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if is running inside a container
if [ -f /.dockerenv ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if is running inside a container
if [ -f /.dockerenv ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if something is running on port 80
if ss -tulnp | grep ':80 ' >/dev/null; then
echo "Error: something is already running on port 80" >&2
exit 1
fi
# check if something is running on port 80
if ss -tulnp | grep ':80 ' >/dev/null; then
echo "Error: something is already running on port 80" >&2
exit 1
fi
# check if something is running on port 443
if ss -tulnp | grep ':443 ' >/dev/null; then
echo "Error: something is already running on port 443" >&2
exit 1
fi
# check if something is running on port 443
if ss -tulnp | grep ':443 ' >/dev/null; then
echo "Error: something is already running on port 443" >&2
exit 1
fi
command_exists() {
command -v "$@" > /dev/null 2>&1
}
command_exists() {
command -v "$@" > /dev/null 2>&1
}
if command_exists docker; then
echo "Docker already installed"
else
curl -sSL https://get.docker.com | sh
fi
docker swarm leave --force 2>/dev/null
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv4" ]; then
echo "$ipv4"
if command_exists docker; then
echo "Docker already installed"
else
# Try to get IPv6
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv6" ]; then
echo "$ipv6"
curl -sSL https://get.docker.com | sh
fi
docker swarm leave --force 2>/dev/null
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv4" ]; then
echo "$ipv4"
else
# Try to get IPv6
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv6" ]; then
echo "$ipv6"
fi
fi
fi
}
advertise_addr=$(get_ip)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized"
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
docker pull dokploy/dokploy:canary
# Installation
docker service create \
--name dokploy \
--replicas 1 \
--network dokploy-network \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
--publish published=3000,target=3000,mode=host \
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
-e RELEASE_TAG=canary \
dokploy/dokploy:canary
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
fi
}
formatted_addr=$(format_ip_for_url "$advertise_addr")
echo ""
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
echo ""
}
advertise_addr=$(get_ip)
update_dokploy() {
echo "Updating Dokploy..."
# Pull the latest canary image
docker pull dokploy/dokploy:canary
docker swarm init --advertise-addr $advertise_addr
# Update the service
docker service update --image dokploy/dokploy:canary dokploy
echo "Swarm initialized"
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
docker pull dokploy/dokploy:canary
# Installation
docker service create \
--name dokploy \
--replicas 1 \
--network dokploy-network \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
--publish published=3000,target=3000,mode=host \
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
-e RELEASE_TAG=canary \
dokploy/dokploy:canary
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
fi
echo "Dokploy has been updated to the latest canary version."
}
formatted_addr=$(format_ip_for_url "$advertise_addr")
echo ""
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
echo ""
# Main script execution
if [ "$1" = "update" ]; then
update_dokploy
else
install_dokploy
fi

View File

@@ -1,97 +1,117 @@
#!/bin/bash
if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root" >&2
exit 1
fi
# check if is Mac OS
if [ "$(uname)" = "Darwin" ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if is running inside a container
if [ -f /.dockerenv ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if something is running on port 80
if ss -tulnp | grep ':80 ' >/dev/null; then
echo "Error: something is already running on port 80" >&2
exit 1
fi
# check if something is running on port 443
if ss -tulnp | grep ':443 ' >/dev/null; then
echo "Error: something is already running on port 443" >&2
exit 1
fi
command_exists() {
command -v "$@" > /dev/null 2>&1
}
if command_exists docker; then
echo "Docker already installed"
else
curl -sSL https://get.docker.com | sh
fi
docker swarm leave --force 2>/dev/null
advertise_addr=$(curl -s ifconfig.me)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized"
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
docker pull dokploy/dokploy:feature
# Installation
docker service create \
--name dokploy \
--replicas 1 \
--network dokploy-network \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
--publish published=3000,target=3000,mode=host \
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
-e RELEASE_TAG=feature \
dokploy/dokploy:feature
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
install_dokploy() {
if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root" >&2
exit 1
fi
# check if is Mac OS
if [ "$(uname)" = "Darwin" ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if is running inside a container
if [ -f /.dockerenv ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if something is running on port 80
if ss -tulnp | grep ':80 ' >/dev/null; then
echo "Error: something is already running on port 80" >&2
exit 1
fi
# check if something is running on port 443
if ss -tulnp | grep ':443 ' >/dev/null; then
echo "Error: something is already running on port 443" >&2
exit 1
fi
command_exists() {
command -v "$@" > /dev/null 2>&1
}
if command_exists docker; then
echo "Docker already installed"
else
curl -sSL https://get.docker.com | sh
fi
docker swarm leave --force 2>/dev/null
advertise_addr=$(curl -s ifconfig.me)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized"
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
docker pull dokploy/dokploy:feature
# Installation
docker service create \
--name dokploy \
--replicas 1 \
--network dokploy-network \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
--publish published=3000,target=3000,mode=host \
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
-e RELEASE_TAG=feature \
dokploy/dokploy:feature
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
fi
}
formatted_addr=$(format_ip_for_url "$advertise_addr")
echo ""
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
echo ""
}
formatted_addr=$(format_ip_for_url "$advertise_addr")
echo ""
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
echo ""
update_dokploy() {
echo "Updating Dokploy..."
# Pull the latest feature image
docker pull dokploy/dokploy:feature
# Update the service
docker service update --image dokploy/dokploy:feature dokploy
echo "Dokploy has been updated to the latest feature version."
}
# Main script execution
if [ "$1" = "update" ]; then
update_dokploy
else
install_dokploy
fi

View File

@@ -1,112 +1,130 @@
#!/bin/bash
install_dokploy() {
if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root" >&2
exit 1
fi
if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root" >&2
exit 1
fi
# check if is Mac OS
if [ "$(uname)" = "Darwin" ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if is Mac OS
if [ "$(uname)" = "Darwin" ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if is running inside a container
if [ -f /.dockerenv ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if something is running on port 80
if ss -tulnp | grep ':80 ' >/dev/null; then
echo "Error: something is already running on port 80" >&2
exit 1
fi
# check if is running inside a container
if [ -f /.dockerenv ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if something is running on port 443
if ss -tulnp | grep ':443 ' >/dev/null; then
echo "Error: something is already running on port 443" >&2
exit 1
fi
# check if something is running on port 80
if ss -tulnp | grep ':80 ' >/dev/null; then
echo "Error: something is already running on port 80" >&2
exit 1
fi
command_exists() {
command -v "$@" > /dev/null 2>&1
}
# check if something is running on port 443
if ss -tulnp | grep ':443 ' >/dev/null; then
echo "Error: something is already running on port 443" >&2
exit 1
fi
command_exists() {
command -v "$@" > /dev/null 2>&1
}
if command_exists docker; then
echo "Docker already installed"
else
curl -sSL https://get.docker.com | sh
fi
docker swarm leave --force 2>/dev/null
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv4" ]; then
echo "$ipv4"
if command_exists docker; then
echo "Docker already installed"
else
# Try to get IPv6
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv6" ]; then
echo "$ipv6"
curl -sSL https://get.docker.com | sh
fi
docker swarm leave --force 2>/dev/null
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv4" ]; then
echo "$ipv4"
else
# Try to get IPv6
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv6" ]; then
echo "$ipv6"
fi
fi
fi
}
advertise_addr=$(get_ip)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized"
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
docker pull dokploy/dokploy:latest
# Installation
docker service create \
--name dokploy \
--replicas 1 \
--network dokploy-network \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
--publish published=3000,target=3000,mode=host \
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
dokploy/dokploy:latest
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
fi
}
formatted_addr=$(format_ip_for_url "$advertise_addr")
echo ""
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
}
advertise_addr=$(get_ip)
update_dokploy() {
echo "Updating Dokploy..."
# Pull the latest image
docker pull dokploy/dokploy:latest
docker swarm init --advertise-addr $advertise_addr
# Update the service
docker service update --image dokploy/dokploy:latest dokploy
echo "Swarm initialized"
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
docker pull dokploy/dokploy:latest
# Installation
docker service create \
--name dokploy \
--replicas 1 \
--network dokploy-network \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
--publish published=3000,target=3000,mode=host \
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
dokploy/dokploy:latest
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
fi
echo "Dokploy has been updated to the latest version."
}
formatted_addr=$(format_ip_for_url "$advertise_addr")
echo ""
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
echo ""
# Main script execution
if [ "$1" = "update" ]; then
update_dokploy
else
install_dokploy
fi