Compare commits

...

162 Commits

Author SHA1 Message Date
Mauricio Siu
213fa08210 Merge pull request #382 from Dokploy/canary
v0.7.2
2024-08-26 15:52:49 -06:00
Mauricio Siu
1eed1a356d chore(version): bump version 2024-08-26 15:41:28 -06:00
Mauricio Siu
a8f21ad717 Merge pull request #381 from Dokploy/security/apply-validation-when-creating-admin
refactor(auth): add validation to prevent create another admin when a…
2024-08-26 15:32:29 -06:00
Mauricio Siu
c1420bd6d8 chore: apply biome 2024-08-26 15:22:51 -06:00
Mauricio Siu
74ea9debd5 refactor(auth): add validation to prevent create another admin when a admin is present 2024-08-26 15:19:30 -06:00
Mauricio Siu
1df9f1f4df Merge pull request #361 from songtianlun/canary
feat: add supbase template
2024-08-24 20:55:02 -06:00
songtianlun
f06ac587c9 Merge branch 'upsteam_canary' into canary 2024-08-25 01:40:35 +08:00
songtianlun
c084cf84a0 fix: supabase generate annon and service jwt by servicekey 2024-08-24 00:43:00 +08:00
songtianlun
5e5cbdeef9 fix: supabase generate annon and service jwt by servicekey 2024-08-24 00:41:14 +08:00
Mauricio Siu
24929d8a4d Merge pull request #371 from freidev/canary
feat(template): add Aptabase template with ClickHouse and PostgreSQLCanary
2024-08-22 15:05:34 -06:00
Freilyn Bernabe
1e6e85ed5b style: run code formatting on two files 2024-08-22 16:08:48 -04:00
Freilyn Bernabe
137edf1250 feat: remove container_name prop 2024-08-22 15:12:03 -04:00
Freilyn Bernabe
b8741f1702 feat(template): aptabase set port 8080 2024-08-22 08:41:12 -04:00
Freilyn Bernabe
ac28aff022 Apply suggestions from code review
Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
2024-08-22 08:38:51 -04:00
songtianlun
217be3c6e9 Merge remote-tracking branch 'origin/canary' into canary 2024-08-22 13:23:38 +08:00
songtianlun
27a0dc3770 fix: supabase ts format 2024-08-22 13:18:06 +08:00
Freilynbp03
53b24534a8 feat(template): add Aptabase template with ClickHouse and PostgreSQL 2024-08-21 21:45:14 -04:00
Freilyn Bernabe
5a3d0f8288 feat: add aptabase template 2024-08-21 16:31:02 -04:00
Mauricio Siu
ff3e3513ef chore: add cloudblast io sponsor 2024-08-21 12:26:44 -06:00
songtianlun
7a1fba38b3 fix: supabase logflareApiKey to 32 passwd 2024-08-22 02:06:42 +08:00
songtianlun
2b65a3c119 fix: supabase MAILER_URLPATHS_CONFIRMATION env path 2024-08-22 01:51:32 +08:00
songtianlun
6d841497cc fix: remove SECRET_KEY_BASE global env 2024-08-22 01:47:33 +08:00
songtianlun
d37dc7c372 fix: supabase log vector.yml 2024-08-22 01:20:24 +08:00
songtianlun
f3e0cf861f fix: supbase hello function demo 2024-08-22 01:17:05 +08:00
songtianlun
e2578e5794 fix: supbase add init/data.sql 2024-08-22 01:13:38 +08:00
TianLun Song
16deec381c fix: ci replace circle config 2024-08-20 20:51:20 +08:00
songtianlun
91dc35a138 fix: supbase index.ts format 2024-08-20 20:39:08 +08:00
songtianlun
acfa032e61 fix: supabase dashboard passwd 2024-08-20 20:39:08 +08:00
songtianlun
83ee4b2c59 fix: ci set branch name 2024-08-20 20:39:08 +08:00
Mauricio Siu
d5c6a601d8 Merge pull request #367 from Dokploy/canary
v0.7.1
2024-08-19 16:03:39 -06:00
Mauricio Siu
afbe42a577 Update package.json 2024-08-19 14:12:14 -06:00
Mauricio Siu
866f700abf Merge pull request #365 from Dokploy/31-are-there-any-plans-to-support-wildcard-dns-records
31 are there any plans to support wildcard dns records
2024-08-19 00:08:55 -06:00
Mauricio Siu
f2b6b33b1f chore: lint 2024-08-18 23:24:20 -06:00
Mauricio Siu
813ffabb8c refactor: check if the traefik dashboard is enabled 2024-08-18 23:19:21 -06:00
Mauricio Siu
b5da9291b4 feat: add enviroment variables editor to traefik 2024-08-18 23:18:54 -06:00
songtianlun
b0d604d12b fix: supabase postgres passwd 2024-08-19 11:33:26 +08:00
Mauricio Siu
e9f40e1644 Merge pull request #364 from Dokploy/354-support-soketi
feat: add soketi template
2024-08-18 20:53:55 -06:00
Mauricio Siu
61d520c239 feat: add data templates 2024-08-18 20:46:18 -06:00
Mauricio Siu
124a884d2e feat: add soketi template 2024-08-18 20:43:30 -06:00
Mauricio Siu
b5e4b9af60 Merge pull request #363 from Dokploy/fix/domains-schema-templates
refactor(templates): use domains tab instead of envs
2024-08-18 19:58:23 -06:00
Mauricio Siu
840c24e3ca eslint 2024-08-18 19:51:59 -06:00
Mauricio Siu
d300eb73fb chore: add license 2024-08-18 19:49:36 -06:00
Mauricio Siu
fb4e06116c chore: add example domains schema templates 2024-08-18 19:49:17 -06:00
Mauricio Siu
2d3b903edc refactor(templates): use domains tab instead of envs 2024-08-18 19:47:19 -06:00
Mauricio Siu
75c13df22f Merge pull request #362 from ErickLuis00/canary
fix: wrong Docker version in Add Node commands
2024-08-18 18:17:49 -06:00
Erick Luis
68b81cb48d pnpm run check 2024-08-18 22:40:05 +00:00
Erick Luis
9d6f2df25a fix: wrong Docker version in Add Node commands 2024-08-18 22:18:45 +00:00
Mauricio Siu
452793c8e5 Merge pull request #359 from Dokploy/canary
v0.7.0
2024-08-18 10:26:52 -06:00
songtianlun
724de2c1b9 fix: ci revert name 2024-08-18 22:07:35 +08:00
songtianlun
86946b6b15 fix: ci revert name 2024-08-18 22:07:02 +08:00
songtianlun
957bb3d3e6 fix: supabase domain is set 2024-08-18 20:41:27 +08:00
Mauricio Siu
38a75b07fb chore(version): bump version 2024-08-17 23:53:36 -06:00
Mauricio Siu
378b93f996 Merge pull request #358 from Dokploy/154-all-pop-ups-when-the-confirmation-behavior-occurs-successfully-the-pop-ups-are-not-closed
refactor: add dialog close on submit success
2024-08-17 23:43:10 -06:00
Mauricio Siu
eb62d124bd refactor: add dialog close on submit success 2024-08-17 23:34:58 -06:00
songtianlun
7558029271 fix: supabase domain in one path 2024-08-18 13:34:37 +08:00
songtianlun
757c28dad1 fix: supabase logo 2024-08-18 13:26:02 +08:00
songtianlun
8d3dc38816 fix: supabase logo 2024-08-18 13:17:05 +08:00
songtianlun
9c8061a447 fix: supabase replace ports to expost 2024-08-18 13:05:44 +08:00
songtianlun
3a8b2867b6 fix: supabase add network 2024-08-18 13:02:25 +08:00
songtianlun
389956d1a2 fix: supabase docker volume content 2024-08-18 12:33:26 +08:00
Mauricio Siu
bf6ed15ba7 Merge pull request #348 from jumkey/patch-1
fix: buildpacks/pack support arm64
2024-08-17 22:30:24 -06:00
Mauricio Siu
31a66ce798 Update Dockerfile 2024-08-17 22:24:39 -06:00
Mauricio Siu
38c1d86e2f refactor(domains): add services to each router 2024-08-17 21:33:23 -06:00
Mauricio Siu
d08e232f50 Merge pull request #356 from Dokploy/216-domains-for-services-created-via-template
216 domains for services created via template
2024-08-17 17:09:34 -06:00
Mauricio Siu
3d49383c42 remove 2024-08-17 17:03:20 -06:00
Mauricio Siu
27706eaae4 fix: lint 2024-08-17 17:00:49 -06:00
Mauricio Siu
c74b5a2677 Merge branch 'canary' into 216-domains-for-services-created-via-template 2024-08-17 16:58:32 -06:00
Mauricio Siu
0374165a7f refactor: remove unused code 2024-08-17 16:57:27 -06:00
Mauricio Siu
b7dad5e1d9 refactor: remove hostname validation 2024-08-17 16:11:03 -06:00
Mauricio Siu
65527bc39a feat: add tests for labels and networks 2024-08-17 16:10:36 -06:00
songtianlun
a84bdd1c8e fix: supabase docker volume path 2024-08-18 01:55:41 +08:00
songtianlun
eb219221be Merge remote-tracking branch 'my/canary' into canary 2024-08-18 01:40:34 +08:00
songtianlun
7b176bd877 feat: add supabase templates 2024-08-18 01:38:25 +08:00
TianLun Song
6970923253 fix docker name 2024-08-18 01:20:24 +08:00
TianLun Song
a3e23d54d8 fix docker push prefix 2024-08-18 01:09:09 +08:00
TianLun Song
8f11207d72 fix push docker prefix 2024-08-18 01:04:22 +08:00
songtianlun
6bd98350d9 feat: add supabase templates 2024-08-18 00:58:09 +08:00
Mauricio Siu
096ef8cd93 Merge pull request #357 from Dokploy/353-domain-binding-form-does-not-accept-russian-domains-and-punycode-domains
refactor: remove hostname regex
2024-08-17 00:41:53 -06:00
Mauricio Siu
d6eafcbb9b refactor: remove hostname regex 2024-08-17 00:34:29 -06:00
Mauricio Siu
c0261384ca refactor: update invalidation cache 2024-08-17 00:29:05 -06:00
Mauricio Siu
ca733addc2 refactor: add dokploy network auutomatically 2024-08-17 00:28:32 -06:00
Mauricio Siu
7497671033 refactor: add dokploy network automatically 2024-08-17 00:28:17 -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
44e75ee7e1 refactor: update deps 2024-08-16 22:10:23 -06:00
Mauricio Siu
6b4d6eac1d chore: bump version 2024-08-16 22:07:02 -06:00
Mauricio Siu
9379d4a31d refactor(domains): update labels 2024-08-15 01:25:36 -06:00
Mauricio Siu
dde799f510 refactor: delete modals 2024-08-15 01:03:58 -06:00
Mauricio Siu
ecb919e109 refactor(domains): make traefik domains generate in a single click 2024-08-15 01:02:11 -06:00
Mauricio Siu
29ca894a97 Merge branch 'canary' into 216-domains-for-services-created-via-template 2024-08-15 00:00:41 -06:00
Mauricio Siu
84ba74a673 refactor: remove migrations 2024-08-15 00:00:34 -06:00
Mauricio Siu
32b0d51e79 refactor: remove migration 2024-08-14 23:59:51 -06:00
Jumkey Chen
3e12e1b1b3 fix: buildpacks/pack support arm64 2024-08-14 19:16:59 +08:00
Mauricio Siu
175e84f50e refactor: update container id 2024-08-13 23:29:51 -06:00
Mauricio Siu
efb646c43d Merge pull request #346 from Dokploy/282-add-option-to-revert-dokploy-version-opt-in-based-auto-updates
282 add option to revert dokploy version opt in based auto updates
2024-08-13 23:12:57 -06:00
Mauricio Siu
fa950dae39 fix(settings): prevent to download the latest image on reload 2024-08-13 23:04:21 -06:00
Mauricio Siu
712ad25e7a feat(permission): add permission to access to ssh key section 2024-08-13 22:19:04 -06:00
Mauricio Siu
35a41e774e Merge pull request #343 from Tuluobo/bugfix/delete_service_with_container
fix(ui): close dialog after templete selected & add config editor line wrapping
2024-08-13 22:02:33 -06:00
Mauricio Siu
c2ac193fbe Merge pull request #344 from Dokploy/340-dokploy-postgres-and-redis-are-exposed
fix(services): set published port 0 to prevent swarm assign random po…
2024-08-13 21:56:12 -06:00
Mauricio Siu
ce3c89a715 Merge pull request #342 from Dokploy/326-dokploy-doesnt-persist-registry-tokens
fix(docker): add root docker to prevent registry delete in each resta…
2024-08-13 21:52:07 -06:00
Mauricio Siu
96f7206a1d fix(services): set published port 0 to prevent swarm assign random ports #340 2024-08-13 21:49:21 -06:00
Mauricio Siu
b7ace886f3 fix(docker): add root docker to prevent registry delete in each restart/update dokploy server #326 2024-08-13 20:40:34 -06:00
Mauricio Siu
5dc330eaa3 Merge pull request #341 from Dokploy/337-incorrect-github-apps-install-link-when-app-name-contain-special-characters
fix(github): use github url to install the application #337
2024-08-13 20:04:46 -06:00
Mauricio Siu
b7f5bee2f8 fix(github): use github url to install the application #337 2024-08-13 19:57:10 -06:00
Tuluobo
19ee5f073b feat: add line wrapping for traefik config editor 2024-08-13 20:51:50 +08:00
Tuluobo
1fd4a6ae80 refactor: close dialog after selected template 2024-08-13 19:38:09 +08:00
Mauricio Siu
3c8a412014 Merge pull request #339 from Vladislav-CS/patch-1
fix: responsive design in the project settings page
2024-08-10 16:00:06 -06:00
Vladislav Popovič
eee617719b Update show-deployments.tsx 2024-08-10 22:19:15 +03:00
Mauricio Siu
fc611946a6 Merge pull request #334 from Vladislav-CS/fix-typos
fix: typos
2024-08-08 10:59:22 -06:00
Vladislav Popovič
af13c84968 Update add-template.tsx 2024-08-08 18:21:35 +03:00
Vladislav Popovič
ddb78ef8dd Update show-ssh-keys.tsx 2024-08-08 18:17:12 +03:00
Mauricio Siu
3590f3bed2 Merge pull request #332 from Dokploy/canary
v0.6.2
2024-08-07 21:48:49 -06:00
Mauricio Siu
c70089ee53 refactor(logs): add error log in build application 2024-08-07 21:41:25 -06:00
Mauricio Siu
161e479a0b chore(version): bump version 2024-08-07 21:08:42 -06:00
Mauricio Siu
bd735bfb64 Merge pull request #331 from Dokploy/322-git-submodules-are-not-cloned
fix(git): add --recursive-submodules flag
2024-08-07 21:09:29 -06:00
Mauricio Siu
85c814620e Merge pull request #330 from Dokploy/fix/add-validation-git-source
fix(git): don't add to ssh known host when is a http or https url
2024-08-07 20:59:42 -06:00
Mauricio Siu
fb013fe4ec fix(git): add --recursive-submodules flag 2024-08-07 20:58:16 -06:00
Mauricio Siu
90a1bd9027 fix(git): don't add to ssh known host when is a http or https url 2024-08-07 20:51:27 -06:00
Mauricio Siu
610ef8f35c Update README.md 2024-08-05 10:23:46 -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
2ffa95b9fa chore(version): bump version 2024-08-03 15:34:09 -06:00
Mauricio Siu
559872c4e4 Merge pull request #316 from Dokploy/315-docs-website-error-dialogclose-must-be-used-within-dialog
fix(docs): update dependencies
2024-08-03 15:32:36 -06:00
Mauricio Siu
85642ed5f2 refactor(docs): add volumes deployments 2024-08-03 15:14:02 -06:00
Mauricio Siu
8bf701d2f2 fix(docs): update dependencies 2024-08-03 15:06:47 -06:00
Mauricio Siu
38809a2034 Merge pull request #314 from Dokploy/fix/add-file-path
fix(template): add missing file path
2024-08-03 15:05:31 -06:00
Mauricio Siu
4bd6ec2232 fix(templates): use filePath instead of mountPath 2024-08-03 14:59:20 -06:00
Mauricio Siu
95899b7208 fix(templates): update path file path 2024-08-02 13:45:20 -06:00
Mauricio Siu
ac26bb95e3 fix(template): add missing file path 2024-08-02 13:40:25 -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
16791a9f4b refactor: remove console log 2024-08-02 10:44:39 -06:00
Mauricio Siu
54ab6e3436 chore(version): bump version 2024-08-02 10:36:06 -06:00
Mauricio Siu
f5ca72ddd7 refactor: add default value to context path 2024-08-02 10:18:41 -06:00
Mauricio Siu
7245e7dfd7 Merge pull request #310 from Dokploy/fix/docker-context
fix(docker-context): add docker context path #284
2024-08-02 09:52:27 -06:00
Mauricio Siu
547d149987 Merge pull request #311 from kucherenko/canary
docs(templates): add documentation about teable and open webui templates
2024-08-02 09:52:05 -06:00
apk
3b2d29514c fix(docs): type in description 2024-08-02 11:20:45 +03:00
apk
115abd378f docs(templates): add information about open webui and about teable to documention 2024-08-02 11:18:32 +03:00
Mauricio Siu
9c101d78d1 Merge pull request #305 from kucherenko/canary
feat(template): add teable template
2024-08-02 01:06:53 -06:00
apk
610f8fa5bc fix(teable): remove network 2024-08-02 09:57:36 +03:00
Mauricio Siu
e201bf12f8 fix(test): add missing prop 2024-08-02 00:28:17 -06:00
Mauricio Siu
bf872200a7 refactor(docker): add type password to input 2024-08-02 00:25:44 -06:00
Mauricio Siu
abc6906349 fix(docker-context): add docker context path #284 2024-08-02 00:22:37 -06:00
apk
9440fd89ae fix(teable): round port for public database 2024-08-02 08:29:53 +03:00
apk
bce1eb8907 fix(teable): port for public database 2024-08-02 08:27:17 +03:00
apk
4bf44b3275 fix(docker-compose): issue with volumes, issue with database connection 2024-08-01 22:05:36 +03:00
Andrey Kucherenko
06355ff089 fix(taeble): version and env variables 2024-08-01 19:27:07 +03:00
Mauricio Siu
95ecf4fe21 Merge pull request #309 from ca110us/canary
fix: align entry point names in configs
2024-08-01 10:19:45 -06:00
ian
d50a6ce76f fix: align entry point names in configs 2024-08-01 23:53:53 +08:00
Mauricio Siu
2d951e0b1f Merge pull request #307 from Dokploy/306-fix-containers-are-deleted-when-dokploy-server-restart
fix(swarm): remove restart policy #306
2024-08-01 09:53:44 -06:00
Mauricio Siu
416de9879b fix(swarm): remove restart policy #306 2024-08-01 09:31:58 -06:00
Mauricio Siu
082aff58a9 chore: add ref query to links 2024-08-01 09:12:25 -06:00
Mauricio Siu
b74666fc2f Merge pull request #304 from lorenzomigliorero/feat/static-buildtype
feat: static buildtype
2024-08-01 09:11:02 -06:00
Andrey Kucherenko
dc626f1a94 chore(lint): fix formating 2024-08-01 13:00:03 +03:00
Andrey Kucherenko
533a5e490f feat(template): add teable template (one more no/low-code database) 2024-08-01 12:13:36 +03:00
Lorenzo Migliorero
cf54e4f5c2 feat: add static migration 2024-08-01 11:10:52 +02:00
Lorenzo Migliorero
d84c808887 feat: add build static 2024-08-01 11:10:28 +02:00
Andrey Kucherenko
89cd35adc6 chore(gitignore): add .idea folder to gitignore file 2024-08-01 11:34:09 +03:00
Mauricio Siu
6299385bb4 Merge pull request #297 from lorenzomigliorero/feat/static-build-phase
feat: new publish directory flag
2024-08-01 02:28:37 -06:00
Lorenzo Migliorero
e6f9867500 fix: error handling 2024-08-01 10:13:29 +02:00
Lorenzo Migliorero
3fdd3ddc74 fix: missing type 2024-07-31 19:27:11 +02:00
Lorenzo Migliorero
6ed379243e feat: add publish directory flag 2024-07-31 19:08:34 +02:00
Mauricio Siu
27256c609a refactor: add validations in domains 2024-07-30 23:31:05 -06:00
Mauricio Siu
c4d59177bf refactor: update domain 2024-07-30 23:20:54 -06:00
Mauricio Siu
3c8ca2b012 feat(domains): add domains to each docker compose service #216 2024-07-30 23:16:52 -06:00
179 changed files with 22613 additions and 1144 deletions

1
.gitignore vendored
View File

@@ -35,6 +35,7 @@ yarn-error.log*
# Editor
.vscode
.idea
# Misc
.DS_Store

View File

@@ -166,20 +166,26 @@ import {
generateRandomDomain,
type Template,
type Schema,
type DomainSchema,
} from "../utils";
export function generate(schema: Schema): Template {
// do your stuff here, like create a new domain, generate random passwords, mounts.
const mainServiceHash = generateHash(schema.projectName);
const randomDomain = generateRandomDomain(schema);
const mainDomain = generateRandomDomain(schema);
const secretBase = generateBase64(64);
const toptKeyBase = generateBase64(32);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 8000,
serviceName: "plausible",
},
];
const envs = [
// If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name.
`PLAUSIBLE_HOST=${randomDomain}`,
"PLAUSIBLE_PORT=8000",
`BASE_URL=http://${randomDomain}`,
`BASE_URL=http://${mainDomain}`,
`SECRET_KEY_BASE=${secretBase}`,
`TOTP_VAULT_KEY=${toptKeyBase}`,
`HASH=${mainServiceHash}`,
@@ -195,6 +201,7 @@ export function generate(schema: Schema): Template {
return {
envs,
mounts,
domains,
};
}
```

View File

@@ -52,7 +52,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& pnpm install -g tsx
# Install buildpacks
RUN curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000
CMD [ "pnpm", "start" ]

26
LICENSE.MD Normal file
View File

@@ -0,0 +1,26 @@
# License
## Core License (Apache License 2.0)
Copyright 2024 Mauricio Siu.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
## Additional Terms for Specific Features
The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
For further inquiries or permissions, please contact us directly.

View File

@@ -1,7 +1,7 @@
<div align="center">
<h1 align="center">Dokploy</h1>
<div>
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://raw.githubusercontent.com/Dokploy/docs/main/public/logo.png" >
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://dokploy.com/og.png" >
</div>
@@ -59,7 +59,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Premium Supporters 🥇
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
<a href="https://supafort.com/" target="_blank"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="190"/></a>
<a href="https://supafort.com/?ref=dokploy" target="_blank"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="190"/></a>
</div>
<!-- Elite Contributors 🥈 -->
@@ -69,13 +69,14 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Supporting Members 🥉
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
<a href="https://lightspeed.run/"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Lightspeed.run"/></a>
</div>
### Community Backers 🤝
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
<a href="https://steamsets.com/"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
</div>
#### Organizations:

View File

@@ -3,13 +3,14 @@ title: "Overview"
description: "Learn how to use Docker Compose with Dokploy"
---
import { Callout } from "fumadocs-ui/components/callout";
Dokploy integrates with Docker Compose and Docker Stack to provide flexible deployment solutions. Whether you are developing locally or deploying at scale, Dokploy facilitates application management through these powerful Docker tools.
### Configuration Methods
Dokploy provides two methods for creating Docker Compose configurations:
- **Docker Compose**: Ideal for standard Docker Compose configurations.
- **Stack**: Geared towards orchestrating applications using Docker Swarm. Note that some Docker Compose features, such as `build`, are not available in this mode.
@@ -21,7 +22,7 @@ Configure the source of your code, the way your application is built, and also m
A code editor within Dokploy allows you to specify environment variables for your Docker Compose file. By default, Dokploy creates a `.env` file in the specified Docker Compose file path.
### Monitoring
### Monitoring
Monitor each service individually within Dokploy. If your application consists of multiple services, each can be monitored separately to ensure optimal performance.
@@ -29,7 +30,6 @@ Monitor each service individually within Dokploy. If your application consists o
Access detailed logs for each service through the Dokploy log viewer, which can help in troubleshooting and ensuring the stability of your services.
### Deployments
You can view the last 10 deployments of your application. When you deploy your application in real time, a new deployment record will be created and it will gradually show you how your application is being built.
@@ -38,7 +38,6 @@ We also offer a button to cancel deployments that are in queue. Note that those
We provide a webhook so that you can trigger your own deployments by pushing to your GitHub, Gitea, GitLab, Bitbucket repository.
### Advanced
This section provides advanced configuration options for experienced users. It includes tools for custom commands within the container and volumes.
@@ -46,4 +45,32 @@ This section provides advanced configuration options for experienced users. It i
- **Command**: Dokploy has a defined command to run the Docker Compose file, ensuring complete control through the UI. However, you can append flags or options to the command.
- **Volumes**: To ensure data persistence across deployments, configure storage volumes for your application.
<ImageZoom src="/assets/images/compose/overview.png" width={800} height={630} quality={100} priority alt='home og image' className="rounded-lg" />
<ImageZoom
src="/assets/images/compose/overview.png"
width={800}
height={630}
quality={100}
priority
alt="home og image"
className="rounded-lg"
/>
<Callout title="Volumes">
Docker volumes are a way to persist data generated and used by Docker containers. They are particularly useful for maintaining data between container restarts or for sharing data among different containers.
To bind a volume to the host machine, you can use the following syntax in your docker-compose.yml file, but this way will clean up the volumes when a new deployment is made:
```yaml
volumes:
- "/folder:/path/in/container" ❌
```
It's recommended to use the ../files folder to ensure your data persists between deployments. For example:
```yaml
volumes:
- "../files/my-database:/var/lib/mysql" ✅
- "../files/my-configs:/etc/my-app/config" ✅
```
</Callout>

View File

@@ -32,7 +32,8 @@ The following templates are available:
- **Metabase**: Open Source Business Intelligence
- **Grafana**: Open Source Dashboard for your metrics
- **Wordpress**: Open Source Content Management System
- **Open WebUI**: Free and Open Source ChatGPT Alternative
- **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres
@@ -42,4 +43,4 @@ We accept contributions to upload new templates to the dokploy repository.
Make sure to follow the guidelines for creating a template:
[Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)
[Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)

View File

@@ -10,10 +10,10 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"fumadocs-core": "12.2.2",
"fumadocs-mdx": "8.2.33",
"fumadocs-openapi": "^3.1.3",
"fumadocs-ui": "12.2.2",
"fumadocs-core": "^12.5.6",
"fumadocs-mdx": "^8.2.34",
"fumadocs-openapi": "^3.3.0",
"fumadocs-ui": "^12.5.6",
"lucide-react": "^0.394.0",
"next": "^14.2.4",
"react": "^18.3.1",

View File

@@ -0,0 +1,89 @@
import type { Domain } from "@/server/api/services/domain";
import { createDomainLabels } from "@/server/utils/docker/domain";
import { describe, expect, it } from "vitest";
describe("createDomainLabels", () => {
const appName = "test-app";
const baseDomain: Domain = {
host: "example.com",
port: 8080,
https: false,
uniqueConfigKey: 1,
certificateType: "none",
applicationId: "",
composeId: "",
domainType: "compose",
serviceName: "test-app",
domainId: "",
path: "/",
createdAt: "",
};
it("should create basic labels for web entrypoint", async () => {
const labels = await createDomainLabels(appName, baseDomain, "web");
expect(labels).toEqual([
"traefik.http.routers.test-app-1-web.rule=Host(`example.com`)",
"traefik.http.routers.test-app-1-web.entrypoints=web",
"traefik.http.services.test-app-1-web.loadbalancer.server.port=8080",
"traefik.http.routers.test-app-1-web.service=test-app-1-web",
]);
});
it("should create labels for websecure entrypoint", async () => {
const labels = await createDomainLabels(appName, baseDomain, "websecure");
expect(labels).toEqual([
"traefik.http.routers.test-app-1-websecure.rule=Host(`example.com`)",
"traefik.http.routers.test-app-1-websecure.entrypoints=websecure",
"traefik.http.services.test-app-1-websecure.loadbalancer.server.port=8080",
"traefik.http.routers.test-app-1-websecure.service=test-app-1-websecure",
]);
});
it("should add redirect middleware for https on web entrypoint", async () => {
const httpsBaseDomain = { ...baseDomain, https: true };
const labels = await createDomainLabels(appName, httpsBaseDomain, "web");
expect(labels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file",
);
});
it("should add Let's Encrypt configuration for websecure with letsencrypt certificate", async () => {
const letsencryptDomain = {
...baseDomain,
https: true,
certificateType: "letsencrypt" as const,
};
const labels = await createDomainLabels(
appName,
letsencryptDomain,
"websecure",
);
expect(labels).toContain(
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
);
});
it("should not add Let's Encrypt configuration for non-letsencrypt certificate", async () => {
const nonLetsencryptDomain = {
...baseDomain,
https: true,
certificateType: "none" as const,
};
const labels = await createDomainLabels(
appName,
nonLetsencryptDomain,
"websecure",
);
expect(labels).not.toContain(
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
);
});
it("should handle different ports correctly", async () => {
const customPortDomain = { ...baseDomain, port: 3000 };
const labels = await createDomainLabels(appName, customPortDomain, "web");
expect(labels).toContain(
"traefik.http.services.test-app-1-web.loadbalancer.server.port=3000",
);
});
});

View File

@@ -0,0 +1,29 @@
import { addDokployNetworkToRoot } from "@/server/utils/docker/domain";
import { describe, expect, it } from "vitest";
describe("addDokployNetworkToRoot", () => {
it("should create network object if networks is undefined", () => {
const result = addDokployNetworkToRoot(undefined);
expect(result).toEqual({ "dokploy-network": { external: true } });
});
it("should add network to an empty object", () => {
const result = addDokployNetworkToRoot({});
expect(result).toEqual({ "dokploy-network": { external: true } });
});
it("should not modify existing network configuration", () => {
const existing = { "dokploy-network": { external: false } };
const result = addDokployNetworkToRoot(existing);
expect(result).toEqual({ "dokploy-network": { external: true } });
});
it("should add network alongside existing networks", () => {
const existing = { "other-network": { external: true } };
const result = addDokployNetworkToRoot(existing);
expect(result).toEqual({
"other-network": { external: true },
"dokploy-network": { external: true },
});
});
});

View File

@@ -0,0 +1,24 @@
import { addDokployNetworkToService } from "@/server/utils/docker/domain";
import { describe, expect, it } from "vitest";
describe("addDokployNetworkToService", () => {
it("should add network to an empty array", () => {
const result = addDokployNetworkToService([]);
expect(result).toEqual(["dokploy-network"]);
});
it("should not add duplicate network to an array", () => {
const result = addDokployNetworkToService(["dokploy-network"]);
expect(result).toEqual(["dokploy-network"]);
});
it("should add network to an existing array with other networks", () => {
const result = addDokployNetworkToService(["other-network"]);
expect(result).toEqual(["other-network", "dokploy-network"]);
});
it("should add network to an object if networks is an object", () => {
const result = addDokployNetworkToService({ "other-network": {} });
expect(result).toEqual({ "other-network": {}, "dokploy-network": {} });
});
});

View File

@@ -78,7 +78,7 @@ test("Should not touch config without host", () => {
expect(originalConfig).toEqual(config);
});
test("Should remove web-secure if https rollback to http", () => {
test("Should remove websecure if https rollback to http", () => {
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
updateServerTraefik(

View File

@@ -40,6 +40,7 @@ const baseApp: ApplicationNested = {
placementSwarm: null,
ports: [],
projectId: "",
publishDirectory: null,
redirects: [],
refreshToken: "",
registry: null,
@@ -54,6 +55,7 @@ const baseApp: ApplicationNested = {
title: null,
updateConfigSwarm: null,
username: null,
dockerContextPath: null,
};
const baseDomain: Domain = {
@@ -65,6 +67,9 @@ const baseDomain: Domain = {
https: false,
path: null,
port: null,
serviceName: "",
composeId: "",
domainType: "application",
uniqueConfigKey: 1,
};

View File

@@ -28,7 +28,7 @@ import {
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } 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";
@@ -52,6 +52,7 @@ export const AddPort = ({
applicationId,
children = <PlusIcon className="h-4 w-4" />,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, isLoading, error, isError } =
@@ -82,6 +83,7 @@ export const AddPort = ({
await utils.application.one.invalidate({
applicationId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create the port");
@@ -89,7 +91,7 @@ export const AddPort = ({
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>{children}</Button>
</DialogTrigger>

View File

@@ -28,7 +28,7 @@ import {
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, Pencil } 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";
@@ -49,6 +49,7 @@ interface Props {
}
export const UpdatePort = ({ portId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { data } = api.port.one.useQuery(
{
@@ -89,6 +90,7 @@ export const UpdatePort = ({ portId }: Props) => {
await utils.application.one.invalidate({
applicationId: response?.applicationId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the port");
@@ -96,7 +98,7 @@ export const UpdatePort = ({ portId }: Props) => {
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<PenBoxIcon className="size-4 text-muted-foreground" />

View File

@@ -23,7 +23,7 @@ import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } 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";
@@ -45,6 +45,7 @@ export const AddRedirect = ({
applicationId,
children = <PlusIcon className="h-4 w-4" />,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, isLoading, error, isError } =
@@ -80,6 +81,7 @@ export const AddRedirect = ({
await utils.application.readTraefikConfig.invalidate({
applicationId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create the redirect");
@@ -87,7 +89,7 @@ export const AddRedirect = ({
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>{children}</Button>
</DialogTrigger>

View File

@@ -23,7 +23,7 @@ import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, Pencil } 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";
@@ -41,6 +41,7 @@ interface Props {
export const UpdateRedirect = ({ redirectId }: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data } = api.redirects.one.useQuery(
{
redirectId,
@@ -84,6 +85,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
await utils.application.one.invalidate({
applicationId: response?.applicationId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the redirect");
@@ -91,7 +93,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<PenBoxIcon className="size-4 text-muted-foreground" />

View File

@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } 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";
@@ -43,7 +43,7 @@ export const AddSecurity = ({
children = <PlusIcon className="h-4 w-4" />,
}: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading, error, isError } =
api.security.create.useMutation();
@@ -72,6 +72,7 @@ export const AddSecurity = ({
await utils.application.readTraefikConfig.invalidate({
applicationId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create the security");
@@ -79,7 +80,7 @@ export const AddSecurity = ({
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>{children}</Button>
</DialogTrigger>

View File

@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, Pencil } 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";
@@ -38,6 +38,7 @@ interface Props {
}
export const UpdateSecurity = ({ securityId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { data } = api.security.one.useQuery(
{
@@ -79,6 +80,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
await utils.application.one.invalidate({
applicationId: response?.applicationId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the security");
@@ -86,7 +88,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<PenBoxIcon className="size-4 text-muted-foreground" />

View File

@@ -46,6 +46,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
<div className="flex flex-col pt-2 relative">
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
<CodeEditor
lineWrapping
value={data || "Empty"}
disabled
className="font-mono"

View File

@@ -144,6 +144,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
<FormLabel>Traefik config</FormLabel>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
routers:

View File

@@ -24,7 +24,7 @@ import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import type React from "react";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -77,6 +77,7 @@ export const AddVolumes = ({
refetch,
children = <PlusIcon className="h-4 w-4" />,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync } = api.mounts.create.useMutation();
const form = useForm<AddMount>({
defaultValues: {
@@ -103,6 +104,7 @@ export const AddVolumes = ({
})
.then(() => {
toast.success("Mount Created");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create the Bind mount");
@@ -117,6 +119,7 @@ export const AddVolumes = ({
})
.then(() => {
toast.success("Mount Created");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create the Volume mount");
@@ -132,6 +135,7 @@ export const AddVolumes = ({
})
.then(() => {
toast.success("Mount Created");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create the File mount");
@@ -142,7 +146,7 @@ export const AddVolumes = ({
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild>
<Button>{children}</Button>
</DialogTrigger>

View File

@@ -2,6 +2,7 @@ import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
@@ -22,7 +23,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Pencil } 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";
@@ -76,6 +77,7 @@ export const UpdateVolume = ({
refetch,
serviceType,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { data } = api.mounts.one.useQuery(
{
@@ -135,6 +137,7 @@ export const UpdateVolume = ({
})
.then(() => {
toast.success("Mount Update");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the Bind mount");
@@ -148,6 +151,7 @@ export const UpdateVolume = ({
})
.then(() => {
toast.success("Mount Update");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the Volume mount");
@@ -162,6 +166,7 @@ export const UpdateVolume = ({
})
.then(() => {
toast.success("Mount Update");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the File mount");
@@ -171,7 +176,7 @@ export const UpdateVolume = ({
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<Pencil className="size-4 text-muted-foreground" />
@@ -291,13 +296,15 @@ export const UpdateVolume = ({
)}
</div>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-update-volume"
type="submit"
>
Update
</Button>
<DialogClose>
<Button
isLoading={isLoading}
form="hook-form-update-volume"
type="submit"
>
Update
</Button>
</DialogClose>
</DialogFooter>
</form>
</Form>

View File

@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -23,6 +24,7 @@ enum BuildType {
heroku_buildpacks = "heroku_buildpacks",
paketo_buildpacks = "paketo_buildpacks",
nixpacks = "nixpacks",
static = "static",
}
const mySchema = z.discriminatedUnion("buildType", [
@@ -34,6 +36,7 @@ const mySchema = z.discriminatedUnion("buildType", [
invalid_type_error: "Dockerfile path is required",
})
.min(1, "Dockerfile required"),
dockerContextPath: z.string().nullable().default(""),
}),
z.object({
buildType: z.literal("heroku_buildpacks"),
@@ -43,6 +46,10 @@ const mySchema = z.discriminatedUnion("buildType", [
}),
z.object({
buildType: z.literal("nixpacks"),
publishDirectory: z.string().optional(),
}),
z.object({
buildType: z.literal("static"),
}),
]);
@@ -73,17 +80,18 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
const buildType = form.watch("buildType");
useEffect(() => {
if (data) {
// TODO: refactor this
if (data.buildType === "dockerfile") {
form.reset({
buildType: data.buildType,
...(data.buildType && {
dockerfile: data.dockerfile || "",
dockerContextPath: data.dockerContextPath || "",
}),
});
} else {
form.reset({
buildType: data.buildType,
publishDirectory: data.publishDirectory || undefined,
});
}
}
@@ -93,7 +101,11 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
await mutateAsync({
applicationId,
buildType: data.buildType,
publishDirectory:
data.buildType === "nixpacks" ? data.publishDirectory : null,
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
dockerContextPath:
data.buildType === "dockerfile" ? data.dockerContextPath : null,
})
.then(async () => {
toast.success("Build type saved");
@@ -171,6 +183,12 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
Paketo Buildpacks
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="static" />
</FormControl>
<FormLabel className="font-normal">Static</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
@@ -179,16 +197,71 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
}}
/>
{buildType === "dockerfile" && (
<>
<FormField
control={form.control}
name="dockerfile"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Docker File</FormLabel>
<FormControl>
<Input
placeholder={"Path of your docker file"}
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="dockerContextPath"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Docker Context Path</FormLabel>
<FormControl>
<Input
placeholder={
"Path of your docker context default: ."
}
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</>
)}
{buildType === "nixpacks" && (
<FormField
control={form.control}
name="dockerfile"
name="publishDirectory"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Docker File</FormLabel>
<div className="space-y-0.5">
<FormLabel>Publish Directory</FormLabel>
<FormDescription>
Allows you to serve a single directory via NGINX after
the build phase. Useful if the final build assets
should be served as a static site.
</FormDescription>
</div>
<FormControl>
<Input
placeholder={"Path of your docker file"}
placeholder={"Publish Directory"}
{...field}
value={field.value ?? ""}
/>

View File

@@ -53,7 +53,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
<div className="flex flex-row items-center gap-2 flex-wrap">
<span>Webhook URL: </span>
<div className="flex flex-row items-center gap-2">
<span className="text-muted-foreground">
<span className="break-all text-muted-foreground">
{`${url}/api/deploy/${data?.refreshToken}`}
</span>
<RefreshToken applicationId={applicationId} />
@@ -72,7 +72,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
{deployments?.map((deployment) => (
<div
key={deployment.deploymentId}
className="flex items-center justify-between rounded-lg border p-4"
className="flex items-center justify-between rounded-lg border p-4 gap-2"
>
<div className="flex flex-col">
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
@@ -87,7 +87,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
{deployment.title}
</span>
{deployment.description && (
<span className="text-sm text-muted-foreground">
<span className="break-all text-sm text-muted-foreground">
{deployment.description}
</span>
)}

View File

@@ -27,13 +27,20 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { domain } from "@/server/db/validations";
import { domain } from "@/server/db/validations/domain";
import { zodResolver } from "@hookform/resolvers/zod";
import { Dices } from "lucide-react";
import type z from "zod";
type Domain = z.infer<typeof domain>;
@@ -60,10 +67,22 @@ export const AddDomain = ({
},
);
const { data: application } = api.application.one.useQuery(
{
applicationId,
},
{
enabled: !!applicationId,
},
);
const { mutateAsync, isError, error, isLoading } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const form = useForm<Domain>({
resolver: zodResolver(domain),
});
@@ -142,9 +161,42 @@ export const AddDomain = ({
render={({ field }) => (
<FormItem>
<FormLabel>Host</FormLabel>
<FormControl>
<Input placeholder="api.dokploy.com" {...field} />
</FormControl>
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
<FormControl>
<Input placeholder="api.dokploy.com" {...field} />
</FormControl>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingGenerate}
onClick={() => {
generateDomain({
appName: application?.appName || "",
})
.then((domain) => {
field.onChange(domain);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
<Dices className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>Generate traefik.me domain</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormMessage />
</FormItem>

View File

@@ -44,12 +44,19 @@ export const DeleteDomain = ({ domainId }: Props) => {
domainId,
})
.then((data) => {
utils.domain.byApplicationId.invalidate({
applicationId: data?.applicationId,
});
utils.application.readTraefikConfig.invalidate({
applicationId: data?.applicationId,
});
if (data?.applicationId) {
utils.domain.byApplicationId.invalidate({
applicationId: data?.applicationId,
});
utils.application.readTraefikConfig.invalidate({
applicationId: data?.applicationId,
});
} else if (data?.composeId) {
utils.domain.byComposeId.invalidate({
composeId: data?.composeId,
});
}
toast.success("Domain delete succesfully");
})
.catch(() => {

View File

@@ -1,79 +0,0 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import { RefreshCcw } from "lucide-react";
import Link from "next/link";
import { GenerateTraefikMe } from "./generate-traefikme";
import { GenerateWildCard } from "./generate-wildcard";
interface Props {
applicationId: string;
}
export const GenerateDomain = ({ applicationId }: Props) => {
return (
<Dialog>
<DialogTrigger className="" asChild>
<Button variant="secondary">
Generate Domain
<RefreshCcw className="size-4 text-muted-foreground " />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Generate Domain</DialogTitle>
<DialogDescription>
Generate Domains for your applications
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 w-full">
<ul className="flex flex-col gap-4">
<li className="flex flex-row items-center gap-4">
<div className="flex flex-col gap-2">
<div className="text-base font-bold">
1. Generate TraefikMe Domain
</div>
<div className="text-sm text-muted-foreground">
This option generates a free domain provided by{" "}
<Link
href="https://traefik.me"
className="text-primary"
target="_blank"
>
TraefikMe
</Link>
. We recommend using this for quick domain testing or if you
don't have a domain yet.
</div>
</div>
</li>
{/* <li className="flex flex-row items-center gap-4">
<div className="flex flex-col gap-2">
<div className="text-base font-bold">
2. Use Wildcard Domain
</div>
<div className="text-sm text-muted-foreground">
To use this option, you need to set up an 'A' record in your
domain provider. For example, create a record for
*.yourdomain.com.
</div>
</div>
</li> */}
</ul>
<div className="flex flex-row gap-4 w-full">
<GenerateTraefikMe applicationId={applicationId} />
{/* <GenerateWildCard applicationId={applicationId} /> */}
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,69 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { RefreshCcw } from "lucide-react";
import React from "react";
import { toast } from "sonner";
interface Props {
applicationId: string;
}
export const GenerateTraefikMe = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } = api.domain.generateDomain.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Generate Domain
<RefreshCcw className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to generate a new domain?
</AlertDialogTitle>
<AlertDialogDescription>
This will generate a new domain and will be used to access to the
application
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
applicationId,
})
.then((data) => {
utils.domain.byApplicationId.invalidate({
applicationId: applicationId,
});
utils.application.readTraefikConfig.invalidate({
applicationId: applicationId,
});
toast.success("Generated Domain succesfully");
})
.catch(() => {
toast.error("Error to generate Domain");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -1,69 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { SquareAsterisk } from "lucide-react";
import React from "react";
import { toast } from "sonner";
interface Props {
applicationId: string;
}
export const GenerateWildCard = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } = api.domain.generateWildcard.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Generate Wildcard Domain
<SquareAsterisk className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to generate a new wildcard domain?
</AlertDialogTitle>
<AlertDialogDescription>
This will generate a new domain and will be used to access to the
application
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
applicationId,
})
.then((data) => {
utils.domain.byApplicationId.invalidate({
applicationId: applicationId,
});
utils.application.readTraefikConfig.invalidate({
applicationId: applicationId,
});
toast.success("Generated Domain succesfully");
})
.catch((e) => {
toast.error(`Error to generate Domain: ${e.message}`);
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -12,7 +12,6 @@ import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
import Link from "next/link";
import { AddDomain } from "./add-domain";
import { DeleteDomain } from "./delete-domain";
import { GenerateDomain } from "./generate-domain";
interface Props {
applicationId: string;
@@ -46,9 +45,6 @@ export const ShowDomains = ({ applicationId }: Props) => {
</Button>
</AddDomain>
)}
{data && data?.length > 0 && (
<GenerateDomain applicationId={applicationId} />
)}
</div>
</CardHeader>
<CardContent className="flex w-full flex-row gap-4">
@@ -65,8 +61,6 @@ export const ShowDomains = ({ applicationId }: Props) => {
<GlobeIcon className="size-4" /> Add Domain
</Button>
</AddDomain>
<GenerateDomain applicationId={applicationId} />
</div>
</div>
) : (

View File

@@ -114,7 +114,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input placeholder="Password" {...field} />
<Input placeholder="Password" {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, SquarePen } 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";
@@ -41,6 +41,7 @@ interface Props {
}
export const UpdateApplication = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } =
api.application.update.useMutation();
@@ -79,6 +80,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
utils.application.one.invalidate({
applicationId: applicationId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the application");
@@ -87,7 +89,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<SquarePen className="size-4 text-muted-foreground" />

View File

@@ -0,0 +1,441 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { domainCompose } from "@/server/db/validations/domain";
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import type z from "zod";
type Domain = z.infer<typeof domainCompose>;
export type CacheType = "fetch" | "cache";
interface Props {
composeId: string;
domainId?: string;
children: React.ReactNode;
}
export const AddDomainCompose = ({
composeId,
domainId = "",
children,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache");
const utils = api.useUtils();
const { data, refetch } = api.domain.one.useQuery(
{
domainId,
},
{
enabled: !!domainId,
},
);
const { data: compose } = api.compose.one.useQuery(
{
composeId,
},
{
enabled: !!composeId,
},
);
const {
data: services,
isFetching: isLoadingServices,
error: errorServices,
refetch: refetchServices,
} = api.compose.loadServices.useQuery(
{
composeId,
type: cacheType,
},
{
retry: false,
refetchOnWindowFocus: false,
},
);
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const { mutateAsync, isError, error, isLoading } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
const form = useForm<Domain>({
resolver: zodResolver(domainCompose),
});
const https = form.watch("https");
useEffect(() => {
if (data) {
form.reset({
...data,
/* Convert null to undefined */
path: data?.path || undefined,
port: data?.port || undefined,
serviceName: data?.serviceName || undefined,
});
}
if (!domainId) {
form.reset({});
}
}, [form, form.reset, data, isLoading]);
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
error: domainId
? "Error to update the domain"
: "Error to create the domain",
submit: domainId ? "Update" : "Create",
dialogDescription: domainId
? "In this section you can edit a domain"
: "In this section you can add domains",
};
const onSubmit = async (data: Domain) => {
await mutateAsync({
domainId,
composeId,
domainType: "compose",
...data,
})
.then(async () => {
await utils.domain.byComposeId.invalidate({
composeId,
});
toast.success(dictionary.success);
if (domainId) {
refetch();
}
setIsOpen(false);
})
.catch(() => {
toast.error(dictionary.error);
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild>
{children}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Domain</DialogTitle>
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 "
>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
{errorServices && (
<AlertBlock
type="warning"
className="[overflow-wrap:anywhere]"
>
{errorServices?.message}
</AlertBlock>
)}
<div className="flex flex-row gap-4 w-full items-end">
<FormField
control={form.control}
name="serviceName"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Service Name</FormLabel>
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a service name" />
</SelectTrigger>
</FormControl>
<SelectContent>
{services?.map((service, index) => (
<SelectItem
value={service}
key={`${service}-${index}`}
>
{service}
</SelectItem>
))}
<SelectItem value="none" disabled>
Empty
</SelectItem>
</SelectContent>
</Select>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "fetch") {
refetchServices();
} else {
setCacheType("fetch");
}
}}
>
<RefreshCw className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Fetch: Will clone the repository and load the
services
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "cache") {
refetchServices();
} else {
setCacheType("cache");
}
}}
>
<DatabaseZap className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Cache: If you previously deployed this
compose, it will read the services from the
last deployment/fetch from the repository
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="host"
render={({ field }) => (
<FormItem>
<FormLabel>Host</FormLabel>
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
<FormControl>
<Input placeholder="api.dokploy.com" {...field} />
</FormControl>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingGenerate}
onClick={() => {
generateDomain({
appName: compose?.appName || "",
})
.then((domain) => {
field.onChange(domain);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
<Dices className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>Generate traefik.me domain</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="path"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Path</FormLabel>
<FormControl>
<Input placeholder={"/"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="port"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Container Port</FormLabel>
<FormControl>
<Input
placeholder={"3000"}
{...field}
onChange={(e) => {
field.onChange(Number.parseInt(e.target.value));
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
{https && (
<FormField
control={form.control}
name="certificateType"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Certificate</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a certificate" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value={"letsencrypt"}>
Letsencrypt (Default)
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<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>
<DialogFooter>
<Button
isLoading={form.formState.isSubmitting}
form="hook-form"
type="submit"
>
{dictionary.submit}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,111 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
import Link from "next/link";
import { DeleteDomain } from "../../application/domains/delete-domain";
import { AddDomainCompose } from "./add-domain";
interface Props {
composeId: string;
}
export const ShowDomainsCompose = ({ composeId }: Props) => {
const { data } = api.domain.byComposeId.useQuery(
{
composeId,
},
{
enabled: !!composeId,
},
);
return (
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center flex-wrap gap-4 justify-between">
<div className="flex flex-col gap-1">
<CardTitle className="text-xl">Domains</CardTitle>
<CardDescription>
Domains are used to access to the application
</CardDescription>
</div>
<div className="flex flex-row gap-4 flex-wrap">
{data && data?.length > 0 && (
<AddDomainCompose composeId={composeId}>
<Button>
<GlobeIcon className="size-4" /> Add Domain
</Button>
</AddDomainCompose>
)}
</div>
</CardHeader>
<CardContent className="flex w-full flex-row gap-4">
{data?.length === 0 ? (
<div className="flex w-full flex-col items-center justify-center gap-3">
<GlobeIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To access to the application is required to set at least 1
domain
</span>
<div className="flex flex-row gap-4 flex-wrap">
<AddDomainCompose composeId={composeId}>
<Button>
<GlobeIcon className="size-4" /> Add Domain
</Button>
</AddDomainCompose>
</div>
</div>
) : (
<div className="flex w-full flex-col gap-4">
{data?.map((item) => {
return (
<div
key={item.domainId}
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
>
<Link target="_blank" href={`http://${item.host}`}>
<ExternalLink className="size-5" />
</Link>
<Button variant="outline" disabled>
{item.serviceName}
</Button>
<Input disabled value={item.host} />
<Button variant="outline" disabled>
{item.path}
</Button>
<Button variant="outline" disabled>
{item.port}
</Button>
<Button variant="outline" disabled>
{item.https ? "HTTPS" : "HTTP"}
</Button>
<div className="flex flex-row gap-1">
<AddDomainCompose
composeId={composeId}
domainId={item.domainId}
>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
</Button>
</AddDomainCompose>
<DeleteDomain domainId={item.domainId} />
</div>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
</div>
);
};

View File

@@ -72,7 +72,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
.then(async () => {
toast.success("Compose config Updated");
refetch();
await utils.compose.allServices.invalidate({
await utils.compose.getConvertedCompose.invalidate({
composeId,
});
})

View File

@@ -5,6 +5,7 @@ import { GitBranch, LockIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { ComposeFileEditor } from "../compose-file-editor";
import { ShowConvertedCompose } from "../show-converted-compose";
import { SaveGitProviderCompose } from "./save-git-provider-compose";
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
@@ -29,7 +30,8 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
Select the source of your code
</p>
</div>
<div className="hidden space-y-1 text-sm font-normal md:block">
<div className="hidden space-y-1 text-sm font-normal md:flex flex-row items-center gap-2">
<ShowConvertedCompose composeId={composeId} />
<GitBranch className="size-6 text-muted-foreground" />
</div>
</CardTitle>

View File

@@ -0,0 +1,87 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import { Puzzle, RefreshCw } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
interface Props {
composeId: string;
}
export const ShowConvertedCompose = ({ composeId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const {
data: compose,
error,
isError,
refetch,
} = api.compose.getConvertedCompose.useQuery(
{ composeId },
{
retry: false,
},
);
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button className="max-lg:w-full" variant="outline">
<Puzzle className="h-4 w-4" />
Preview Compose
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
<DialogHeader>
<DialogTitle>Converted Compose</DialogTitle>
<DialogDescription>
Preview your docker-compose file with added domains. Note: At least
one domain must be specified for this conversion to take effect.
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="flex flex-row gap-2 justify-end">
<Button
variant="secondary"
isLoading={isLoading}
onClick={() => {
mutateAsync({ composeId })
.then(() => {
refetch();
toast.success("Fetched source type");
})
.catch((err) => {
toast.error("Error to fetch source type", {
description: err.message,
});
});
}}
>
Refresh <RefreshCw className="ml-2 h-4 w-4" />
</Button>
</div>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
</DialogContent>
</Dialog>
);
};

View File

@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { SquarePen } 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";
@@ -41,6 +41,7 @@ interface Props {
}
export const UpdateCompose = ({ composeId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } =
api.compose.update.useMutation();
@@ -79,6 +80,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
utils.compose.one.invalidate({
composeId: composeId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the Compose");
@@ -87,7 +89,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<SquarePen className="size-4 text-muted-foreground" />

View File

@@ -36,7 +36,7 @@ import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, PenBoxIcon, Pencil } 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";
@@ -57,6 +57,7 @@ interface Props {
}
export const UpdateBackup = ({ backupId, refetch }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data, isLoading } = api.destination.all.useQuery();
const { data: backup } = api.backup.one.useQuery(
{
@@ -105,6 +106,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
.then(async () => {
toast.success("Backup Updated");
refetch();
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the backup");
@@ -112,7 +114,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />

View File

@@ -104,6 +104,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
</FormDescription>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
routers:

View File

@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, SquarePen } 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";
@@ -41,6 +41,7 @@ interface Props {
}
export const UpdateMongo = ({ mongoId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } =
api.mongo.update.useMutation();
@@ -79,6 +80,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
utils.mongo.one.invalidate({
mongoId: mongoId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update mongo database");
@@ -87,7 +89,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<SquarePen className="size-4 text-muted-foreground" />

View File

@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, SquarePen } 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";
@@ -41,6 +41,7 @@ interface Props {
}
export const UpdatePostgres = ({ postgresId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } =
api.postgres.update.useMutation();
@@ -79,6 +80,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
utils.postgres.one.invalidate({
postgresId: postgresId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the postgres");
@@ -87,7 +89,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<SquarePen className="size-4 text-muted-foreground" />

View File

@@ -55,6 +55,7 @@ interface Props {
export const AddTemplate = ({ projectId }: Props) => {
const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
const { data } = api.compose.templates.useQuery();
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const { data: tags, isLoading: isLoadingTags } =
@@ -75,14 +76,14 @@ export const AddTemplate = ({ projectId }: Props) => {
}) || [];
return (
<Dialog>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="w-full">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<PuzzleIcon className="size-4 text-muted-foreground" />
<span>Templates</span>
<span>Template</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl p-0">
@@ -283,6 +284,7 @@ export const AddTemplate = ({ projectId }: Props) => {
utils.project.one.invalidate({
projectId,
});
setOpen(false);
})
.catch(() => {
toast.error(

View File

@@ -23,7 +23,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, SquarePen } 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";
@@ -42,6 +42,7 @@ interface Props {
}
export const UpdateProject = ({ projectId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, error, isError } = api.project.update.useMutation();
const { data } = api.project.one.useQuery(
@@ -77,6 +78,7 @@ export const UpdateProject = ({ projectId }: Props) => {
.then(() => {
toast.success("Project updated succesfully");
utils.project.all.invalidate();
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the project");
@@ -85,7 +87,7 @@ export const UpdateProject = ({ projectId }: Props) => {
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"

View File

@@ -23,12 +23,14 @@ export const AddManager = () => {
<div className="flex flex-col gap-2.5 text-sm">
<span>1. Go to your new server and run the following command</span>
<span className="bg-muted rounded-lg p-2 flex justify-between">
curl https://get.docker.com | sh -s -- --version 24.0
curl https://get.docker.com | sh -s -- --version {data?.version}
<button
type="button"
className="self-center"
onClick={() => {
copy("curl https://get.docker.com | sh -s -- --version 24.0");
copy(
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
);
toast.success("Copied to clipboard");
}}
>
@@ -43,12 +45,12 @@ export const AddManager = () => {
cluster
</span>
<span className="bg-muted rounded-lg p-2 flex">
{data}
{data?.command}
<button
type="button"
className="self-start"
onClick={() => {
copy(data || "");
copy(data?.command || "");
toast.success("Copied to clipboard");
}}
>

View File

@@ -22,12 +22,14 @@ export const AddWorker = () => {
<div className="flex flex-col gap-2.5 text-sm">
<span>1. Go to your new server and run the following command</span>
<span className="bg-muted rounded-lg p-2 flex justify-between">
curl https://get.docker.com | sh -s -- --version 24.0
curl https://get.docker.com | sh -s -- --version {data?.version}
<button
type="button"
className="self-center"
onClick={() => {
copy("curl https://get.docker.com | sh -s -- --version 24.0");
copy(
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
);
toast.success("Copied to clipboard");
}}
>
@@ -42,12 +44,12 @@ export const AddWorker = () => {
</span>
<span className="bg-muted rounded-lg p-2 flex">
{data}
{data?.command}
<button
type="button"
className="self-start"
onClick={() => {
copy(data || "");
copy(data?.command || "");
toast.success("Copied to clipboard");
}}
>

View File

@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } 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";
@@ -43,6 +43,7 @@ interface Props {
export const UpdateDestination = ({ destinationId }: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data, refetch } = api.destination.one.useQuery(
{
destinationId,
@@ -93,13 +94,14 @@ export const UpdateDestination = ({ destinationId }: Props) => {
toast.success("Destination Updated");
await refetch();
await utils.destination.all.invalidate();
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the Destination");
});
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />

View File

@@ -9,36 +9,11 @@ import {
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { format } from "date-fns";
import { BadgeCheck } from "lucide-react";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import { RemoveGithubApp } from "./remove-github-app";
export const generateName = () => {
const n1 = ["Blue", "Green", "Red", "Orange", "Violet", "Indigo", "Yellow"];
const n2 = [
"One",
"Two",
"Three",
"Four",
"Five",
"Six",
"Seven",
"Eight",
"Nine",
"Zero",
];
return `Dokploy-${n1[Math.round(Math.random() * (n1.length - 1))]}-${
n2[Math.round(Math.random() * (n2.length - 1))]
}`;
};
function slugify(text: string) {
return text
.toLowerCase()
.replace(/[\s\^&*()+=!]+/g, "-")
.replace(/[\$.,*+~()'"!:@^&]+/g, "")
.replace(/-+/g, "-")
.replace(/^-+|-+$/g, "");
}
export const GithubSetup = () => {
const [isOrganization, setIsOrganization] = useState(false);
@@ -52,10 +27,9 @@ export const GithubSetup = () => {
const manifest = JSON.stringify(
{
redirect_url: `${origin}/api/redirect?authId=${data?.authId}`,
name: generateName(),
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
url: origin,
hook_attributes: {
// JUST FOR TESTING
url: `${url}/api/deploy/github`,
// url: `${origin}/api/webhook`, // Aquí especificas la URL del endpoint de tu webhook
},
@@ -95,8 +69,8 @@ export const GithubSetup = () => {
</div>
<div className="flex items-end gap-4 flex-wrap">
<RemoveGithubApp />
{/* <Link
href={`https://github.com/settings/apps/${data?.githubAppName}`}
<Link
href={`${data?.githubAppName}`}
target="_blank"
className={buttonVariants({
className: "w-fit",
@@ -104,7 +78,7 @@ export const GithubSetup = () => {
})}
>
<span className="text-sm">Manage Github App</span>
</Link> */}
</Link>
</div>
</div>
) : (
@@ -119,9 +93,9 @@ export const GithubSetup = () => {
<div className="flex flex-row gap-4">
<Link
href={`https://github.com/apps/${slugify(
data.githubAppName,
)}/installations/new?state=gh_setup:${data?.authId}`}
href={`${
data.githubAppName
}/installations/new?state=gh_setup:${data?.authId}`}
className={buttonVariants({ className: "w-fit" })}
>
Install Github App

View File

@@ -27,7 +27,7 @@ import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Mail, PenBoxIcon } from "lucide-react";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { FieldErrors, useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import {
@@ -41,6 +41,7 @@ interface Props {
export const UpdateNotification = ({ notificationId }: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data, refetch } = api.notification.one.useQuery(
{
notificationId,
@@ -207,6 +208,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
toast.success("Notification Updated");
await utils.notification.all.invalidate();
refetch();
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update a notification");
@@ -214,7 +216,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
}
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />

View File

@@ -22,7 +22,7 @@ export const ShowDestinations = () => {
<CardHeader>
<CardTitle className="text-xl">SSH Keys</CardTitle>
<CardDescription>
Use SSH to beeing able cloning from private repositories.
Use SSH to be able to clone from private repositories.
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 pt-4">

View File

@@ -39,6 +39,7 @@ const addPermissions = z.object({
canAccessToTraefikFiles: z.boolean().optional().default(false),
canAccessToDocker: z.boolean().optional().default(false),
canAccessToAPI: z.boolean().optional().default(false),
canAccessToSSHKeys: z.boolean().optional().default(false),
});
type AddPermissions = z.infer<typeof addPermissions>;
@@ -82,6 +83,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
canAccessToSSHKeys: data.canAccessToSSHKeys,
});
}
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
@@ -98,6 +100,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
accesedServices: data.accesedServices || [],
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
canAccessToSSHKeys: data.canAccessToSSHKeys,
})
.then(async () => {
toast.success("Permissions updated");
@@ -270,6 +273,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="canAccessToSSHKeys"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Access to SSH Keys</FormLabel>
<FormDescription>
Allow to users to access to the SSH Keys section
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="accesedProjects"

View File

@@ -22,6 +22,7 @@ import {
import { api } from "@/utils/api";
import { toast } from "sonner";
import { DockerTerminalModal } from "./web-server/docker-terminal-modal";
import { EditTraefikEnv } from "./web-server/edit-traefik-env";
import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config";
import { ShowModalLogs } from "./web-server/show-modal-logs";
import { ShowServerMiddlewareConfig } from "./web-server/show-server-middleware-config";
@@ -67,6 +68,9 @@ export const WebServer = () => {
const { mutateAsync: updateDockerCleanup } =
api.settings.updateDockerCleanup.useMutation();
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
api.settings.haveTraefikDashboardPortEnabled.useQuery();
return (
<Card className="rounded-lg w-full bg-transparent">
<CardHeader>
@@ -167,37 +171,38 @@ export const WebServer = () => {
<span>View Traefik config</span>
</DropdownMenuItem>
</ShowMainTraefikConfig>
<EditTraefikEnv>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>Modify Env</span>
</DropdownMenuItem>
</EditTraefikEnv>
<DropdownMenuItem
onClick={async () => {
await toggleDashboard({
enableDashboard: true,
enableDashboard: !haveTraefikDashboardPortEnabled,
})
.then(async () => {
toast.success("Dashboard Enabled");
toast.success(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
refetchDashboard();
})
.catch(() => {
toast.error("Error to enable Dashboard");
toast.error(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
});
}}
className="w-full cursor-pointer space-x-3"
>
<span>Enable Dashboard</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={async () => {
await toggleDashboard({
enableDashboard: false,
})
.then(async () => {
toast.success("Dashboard Disabled");
})
.catch(() => {
toast.error("Error to disable Dashboard");
});
}}
className="w-full cursor-pointer space-x-3"
>
<span>Disable Dashboard</span>
<span>
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"}{" "}
Dashboard
</span>
</DropdownMenuItem>
<DockerTerminalModal appName="dokploy-traefik">

View File

@@ -0,0 +1,146 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const schema = z.object({
env: z.string(),
});
type Schema = z.infer<typeof schema>;
interface Props {
children?: React.ReactNode;
}
export const EditTraefikEnv = ({ children }: Props) => {
const [canEdit, setCanEdit] = useState(true);
const { data } = api.settings.readTraefikEnv.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.settings.writeTraefikEnv.useMutation();
const form = useForm<Schema>({
defaultValues: {
env: data || "",
},
disabled: canEdit,
resolver: zodResolver(schema),
});
useEffect(() => {
if (data) {
form.reset({
env: data || "",
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: Schema) => {
await mutateAsync(data.env)
.then(async () => {
toast.success("Traefik Env Updated");
})
.catch(() => {
toast.error("Error to update the traefik env");
});
};
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Update Traefik Env</DialogTitle>
<DialogDescription>Update the traefik env</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-update-server-traefik-config"
onSubmit={form.handleSubmit(onSubmit)}
className="w-full space-y-4 relative overflow-auto"
>
<div className="flex flex-col">
<FormField
control={form.control}
name="env"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Env</FormLabel>
<FormControl>
<CodeEditor
language="properties"
wrapperClassName="h-[35rem] font-mono"
placeholder={`TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=test@localhost.com
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_STORAGE=/etc/dokploy/traefik/dynamic/acme.json
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_HTTP_CHALLENGE=true
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_HTTP_CHALLENGE_PRETTY=true
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_HTTP_CHALLENGE_ENTRYPOINT=web
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_HTTP_CHALLENGE_DNS_CHALLENGE=true
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_HTTP_CHALLENGE_DNS_PROVIDER=cloudflare
`}
{...field}
/>
</FormControl>
<pre>
<FormMessage />
</pre>
<div className="flex justify-end absolute z-50 right-6 top-0">
<Button
className="shadow-sm"
variant="secondary"
type="button"
onClick={async () => {
setCanEdit(!canEdit);
}}
>
{canEdit ? "Unlock" : "Lock"}
</Button>
</div>
</FormItem>
)}
/>
</div>
</form>
<DialogFooter>
<Button
isLoading={isLoading}
disabled={canEdit}
form="hook-form-update-server-traefik-config"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -106,6 +106,7 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
<FormLabel>Traefik config</FormLabel>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`providers:
docker:

View File

@@ -109,6 +109,7 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
<FormLabel>Traefik config</FormLabel>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
routers:

View File

@@ -79,6 +79,16 @@ export const SettingsLayout = ({ children }: Props) => {
},
]
: []),
...(user?.canAccessToSSHKeys
? [
{
title: "SSH Keys",
label: "",
icon: KeyRound,
href: "/dashboard/settings/ssh-keys",
},
]
: []),
]}
/>
</div>

View File

@@ -3,6 +3,7 @@ import { json } from "@codemirror/lang-json";
import { yaml } from "@codemirror/lang-yaml";
import { StreamLanguage } from "@codemirror/language";
import { properties } from "@codemirror/legacy-modes/mode/properties";
import { EditorView } from "@codemirror/view";
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
import { useTheme } from "next-themes";
@@ -10,6 +11,7 @@ interface Props extends ReactCodeMirrorProps {
wrapperClassName?: string;
disabled?: boolean;
language?: "yaml" | "json" | "properties";
lineWrapping?: boolean;
}
export const CodeEditor = ({
@@ -36,6 +38,7 @@ export const CodeEditor = ({
: language === "json"
? json()
: StreamLanguage.define(properties),
props.lineWrapping ? EditorView.lineWrapping : [],
]}
{...props}
editable={!props.disabled}

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "publishDirectory" text;

View File

@@ -0,0 +1 @@
ALTER TYPE "buildType" ADD VALUE 'static';

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "dockerContextPath" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "user" ADD COLUMN "canAccessToSSHKeys" boolean DEFAULT false NOT NULL;

View File

@@ -0,0 +1,15 @@
DO $$ BEGIN
CREATE TYPE "public"."domainType" AS ENUM('compose', 'application');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "domain" ALTER COLUMN "applicationId" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "domain" ADD COLUMN "serviceName" text;--> statement-breakpoint
ALTER TABLE "domain" ADD COLUMN "domainType" "domainType" DEFAULT 'application';--> statement-breakpoint
ALTER TABLE "domain" ADD COLUMN "composeId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "domain" ADD CONSTRAINT "domain_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@@ -0,0 +1 @@
ALTER TABLE "domain" ALTER COLUMN "port" SET DEFAULT 3000;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -190,6 +190,48 @@
"when": 1721979220929,
"tag": "0026_known_dormammu",
"breakpoints": true
},
{
"idx": 27,
"version": "6",
"when": 1722445099203,
"tag": "0027_red_lady_bullseye",
"breakpoints": true
},
{
"idx": 28,
"version": "6",
"when": 1722503439951,
"tag": "0028_jittery_eternity",
"breakpoints": true
},
{
"idx": 29,
"version": "6",
"when": 1722578386823,
"tag": "0029_colossal_zodiak",
"breakpoints": true
},
{
"idx": 30,
"version": "6",
"when": 1723608499147,
"tag": "0030_little_kabuki",
"breakpoints": true
},
{
"idx": 31,
"version": "6",
"when": 1723701656243,
"tag": "0031_steep_vulture",
"breakpoints": true
},
{
"idx": 32,
"version": "6",
"when": 1723705257806,
"tag": "0032_flashy_shadow_king",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.5.1",
"version": "v0.7.2",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -39,6 +39,7 @@
"@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1",
"@codemirror/legacy-modes": "6.4.0",
"@codemirror/view": "6.29.0",
"@dokploy/trpc-openapi": "0.0.4",
"@faker-js/faker": "^8.4.1",
"@hookform/resolvers": "^3.3.4",

View File

@@ -35,7 +35,7 @@ export default async function handler(
.update(admins)
.set({
githubAppId: data.id,
githubAppName: data.name,
githubAppName: data.html_url,
githubClientId: data.client_id,
githubClientSecret: data.client_secret,
githubWebhookSecret: data.webhook_secret,

View File

@@ -2,6 +2,7 @@ import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-c
import { ShowVolumesCompose } from "@/components/dashboard/compose/advanced/show-volumes";
import { DeleteCompose } from "@/components/dashboard/compose/delete-compose";
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-domains";
import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
@@ -34,6 +35,7 @@ type TabState =
| "settings"
| "advanced"
| "deployments"
| "domains"
| "monitoring";
const Service = (
@@ -117,12 +119,13 @@ const Service = (
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-y-scroll justify-start">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
@@ -168,6 +171,12 @@ const Service = (
<ShowDeploymentsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="domains">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomainsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommandCompose composeId={composeId} />

View File

@@ -1,9 +1,12 @@
import { ShowDestinations } from "@/components/dashboard/settings/ssh-keys/show-ssh-keys";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
@@ -26,7 +29,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user || user.rol === "user") {
if (!user) {
return {
redirect: {
permanent: true,
@@ -34,8 +37,45 @@ export async function getServerSideProps(
},
};
}
const { req, res, resolvedUrl } = ctx;
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
return {
props: {},
};
try {
await helpers.project.all.prefetch();
const auth = await helpers.auth.get.fetch();
if (auth.rol === "user") {
const user = await helpers.user.byAuthId.fetch({
authId: auth.id,
});
if (!user.canAccessToSSHKeys) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
}
return {
props: {
trpcState: helpers.dehydrate(),
},
};
} catch (error) {
return {
props: {},
};
}
}

View File

@@ -0,0 +1,5 @@
<svg class="w-12 text-primary" viewBox="0 0 1000 760" xmlns="http://www.w3.org/2000/svg">
<path fill="#1a61ff"
d="M626.7 177.36c-55.8-98.4-197.59-98.4-253.39 0L112.97 636.44H500c0-51.67 41.88-93.55 93.55-93.55h22.09l57.82 93.55h213.57L626.69 177.37Zm-11.06 365.52-70.21-123.82c-20.01-35.28-70.84-35.28-90.85 0l-70.21 123.82H273.58l181.01-319.19c20.01-35.28 70.84-35.28 90.85 0l181.01 319.19H615.66Z"
style="--darkreader-inline-fill:currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,15 @@
<svg width="109" height="113" viewBox="0 0 109 113" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0625L99.1935 40.0625C107.384 40.0625 111.952 49.5226 106.859 55.9372L63.7076 110.284Z" fill="url(#paint0_linear)"/>
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0625L99.1935 40.0625C107.384 40.0625 111.952 49.5226 106.859 55.9372L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
<defs>
<linearGradient id="paint0_linear" x1="53.9738" y1="54.9738" x2="94.1635" y2="71.8293" gradientUnits="userSpaceOnUse">
<stop stop-color="#249361"/>
<stop offset="1" stop-color="#3ECF8E"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="36.1558" y1="30.5779" x2="54.4844" y2="65.0804" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

View File

@@ -191,6 +191,8 @@ export const applicationRouter = createTRPCRouter({
await updateApplication(input.applicationId, {
buildType: input.buildType,
dockerfile: input.dockerfile,
publishDirectory: input.publishDirectory,
dockerContextPath: input.dockerContextPath,
});
return true;

View File

@@ -12,6 +12,7 @@ import {
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { db } from "../../db";
import {
createAdmin,
createUser,
@@ -33,6 +34,14 @@ export const authRouter = createTRPCRouter({
.input(apiCreateAdmin)
.mutation(async ({ ctx, input }) => {
try {
const admin = await db.query.admins.findFirst({});
if (admin) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Admin already exists",
});
}
const newAdmin = await createAdmin(input);
const session = await lucia.createSession(newAdmin.id || "", {});
ctx.res.appendHeader(

View File

@@ -35,14 +35,23 @@ export const clusterRouter = createTRPCRouter({
}),
addWorker: protectedProcedure.query(async ({ input }) => {
const result = await docker.swarmInspect();
return `docker swarm join --token ${
result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`;
const docker_version = await docker.version();
return {
command: `docker swarm join --token ${
result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
addManager: protectedProcedure.query(async ({ input }) => {
const result = await docker.swarmInspect();
return `docker swarm join --token ${
result.JoinTokens.Manager
} ${await getPublicIpWithFallback()}:2377`;
const docker_version = await docker.version();
return {
command: `docker swarm join --token ${
result.JoinTokens.Manager
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
});

View File

@@ -3,6 +3,7 @@ import { db } from "@/server/db";
import {
apiCreateCompose,
apiCreateComposeByTemplate,
apiFetchServices,
apiFindCompose,
apiRandomizeCompose,
apiUpdateCompose,
@@ -15,16 +16,18 @@ import {
import { myQueue } from "@/server/queues/queueSetup";
import { createCommand } from "@/server/utils/builders/compose";
import { randomizeComposeFile } from "@/server/utils/docker/compose";
import { addDomainToCompose, cloneCompose } from "@/server/utils/docker/domain";
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
import { templates } from "@/templates/templates";
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
import {
generatePassword,
loadTemplateModule,
readComposeFile,
readTemplateComposeFile,
} from "@/templates/utils";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import _ from "lodash";
import { nanoid } from "nanoid";
import { findAdmin } from "../services/admin";
@@ -38,6 +41,7 @@ import {
updateCompose,
} from "../services/compose";
import { removeDeploymentsByComposeId } from "../services/deployment";
import { createDomain, findDomainsByComposeId } from "../services/domain";
import { createMount } from "../services/mount";
import { findProjectById } from "../services/project";
import { addNewService, checkServiceAccess } from "../services/user";
@@ -113,10 +117,25 @@ export const composeRouter = createTRPCRouter({
await cleanQueuesByCompose(input.composeId);
}),
allServices: protectedProcedure
.input(apiFindCompose)
loadServices: protectedProcedure
.input(apiFetchServices)
.query(async ({ input }) => {
return await loadServices(input.composeId);
return await loadServices(input.composeId, input.type);
}),
fetchSourceType: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input }) => {
try {
const compose = await findComposeById(input.composeId);
await cloneCompose(compose);
return compose.sourceType;
} catch (err) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to fetch source type",
cause: err,
});
}
}),
randomizeCompose: protectedProcedure
@@ -124,6 +143,17 @@ export const composeRouter = createTRPCRouter({
.mutation(async ({ input }) => {
return await randomizeComposeFile(input.composeId, input.prefix);
}),
getConvertedCompose: protectedProcedure
.input(apiFindCompose)
.query(async ({ input }) => {
const compose = await findComposeById(input.composeId);
const domains = await findDomainsByComposeId(input.composeId);
const composeFile = await addDomainToCompose(compose, domains);
return dump(composeFile, {
lineWidth: 1000,
});
}),
deploy: protectedProcedure
.input(apiFindCompose)
@@ -189,7 +219,7 @@ export const composeRouter = createTRPCRouter({
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
const composeFile = await readComposeFile(input.id);
const composeFile = await readTemplateComposeFile(input.id);
const generate = await loadTemplateModule(input.id as TemplatesKeys);
@@ -206,7 +236,7 @@ export const composeRouter = createTRPCRouter({
const project = await findProjectById(input.projectId);
const projectName = slugify(`${project.name} ${input.id}`);
const { envs, mounts } = generate({
const { envs, mounts, domains } = generate({
serverIp: admin.serverIp,
projectName: projectName,
});
@@ -214,7 +244,7 @@ export const composeRouter = createTRPCRouter({
const compose = await createComposeByTemplate({
...input,
composeFile: composeFile,
env: envs.join("\n"),
env: envs?.join("\n"),
name: input.id,
sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`,
@@ -227,7 +257,8 @@ export const composeRouter = createTRPCRouter({
if (mounts && mounts?.length > 0) {
for (const mount of mounts) {
await createMount({
mountPath: mount.mountPath,
filePath: mount.filePath,
mountPath: "",
content: mount.content,
serviceId: compose.composeId,
serviceType: "compose",
@@ -236,6 +267,17 @@ export const composeRouter = createTRPCRouter({
}
}
if (domains && domains?.length > 0) {
for (const domain of domains) {
await createDomain({
...domain,
domainType: "compose",
certificateType: "none",
composeId: compose.composeId,
});
}
}
return null;
}),

View File

@@ -1,8 +1,12 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiCreateDomain,
apiCreateTraefikMeDomain,
apiFindCompose,
apiFindDomain,
apiFindDomainByApplication,
apiFindDomainByCompose,
apiFindOneApplication,
apiUpdateDomain,
} from "@/server/db/schema";
import { manageDomain, removeDomain } from "@/server/utils/traefik/domain";
@@ -12,8 +16,8 @@ import {
createDomain,
findDomainById,
findDomainsByApplicationId,
generateDomain,
generateWildcard,
findDomainsByComposeId,
generateTraefikMeDomain,
removeDomainById,
updateDomainById,
} from "../services/domain";
@@ -33,27 +37,30 @@ export const domainRouter = createTRPCRouter({
}
}),
byApplicationId: protectedProcedure
.input(apiFindDomainByApplication)
.input(apiFindOneApplication)
.query(async ({ input }) => {
return await findDomainsByApplicationId(input.applicationId);
}),
byComposeId: protectedProcedure
.input(apiFindCompose)
.query(async ({ input }) => {
return await findDomainsByComposeId(input.composeId);
}),
generateDomain: protectedProcedure
.input(apiFindDomainByApplication)
.input(apiCreateTraefikMeDomain)
.mutation(async ({ input }) => {
return generateDomain(input);
}),
generateWildcard: protectedProcedure
.input(apiFindDomainByApplication)
.mutation(async ({ input }) => {
return generateWildcard(input);
return generateTraefikMeDomain(input.appName);
}),
update: protectedProcedure
.input(apiUpdateDomain)
.mutation(async ({ input }) => {
const result = await updateDomainById(input.domainId, input);
const domain = await findDomainById(input.domainId);
const application = await findApplicationById(domain.applicationId);
await manageDomain(application, domain);
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
await manageDomain(application, domain);
}
return result;
}),
one: protectedProcedure.input(apiFindDomain).query(async ({ input }) => {
@@ -64,7 +71,9 @@ export const domainRouter = createTRPCRouter({
.mutation(async ({ input }) => {
const domain = await findDomainById(input.domainId);
const result = await removeDomainById(input.domainId);
await removeDomain(domain.application.appName, domain.uniqueConfigKey);
if (domain.application) {
await removeDomain(domain.application.appName, domain.uniqueConfigKey);
}
return result;
}),

View File

@@ -15,11 +15,13 @@ import {
cleanUpSystemPrune,
cleanUpUnusedImages,
cleanUpUnusedVolumes,
prepareEnvironmentVariables,
startService,
stopService,
} from "@/server/utils/docker/utils";
import { recreateDirectory } from "@/server/utils/filesystem/directory";
import { sendDockerCleanupNotifications } from "@/server/utils/notifications/docker-cleanup";
import { execAsync } from "@/server/utils/process/execAsync";
import { spawnAsync } from "@/server/utils/process/spawnAsync";
import {
readConfig,
@@ -36,6 +38,7 @@ import {
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { z } from "zod";
import { appRouter } from "../root";
import { findAdmin, updateAdmin } from "../services/admin";
import {
@@ -49,14 +52,10 @@ import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
export const settingsRouter = createTRPCRouter({
reloadServer: adminProcedure.mutation(async () => {
await spawnAsync("docker", [
"service",
"update",
"--force",
"--image",
getDokployImage(),
"dokploy",
]);
const { stdout } = await execAsync(
"docker service inspect dokploy --format '{{.ID}}'",
);
await execAsync(`docker service update --force ${stdout.trim()}`);
return true;
}),
reloadTraefik: adminProcedure.mutation(async () => {
@@ -72,7 +71,9 @@ export const settingsRouter = createTRPCRouter({
toggleDashboard: adminProcedure
.input(apiEnableDashboard)
.mutation(async ({ input }) => {
await initializeTraefik(input.enableDashboard);
await initializeTraefik({
enableDashboard: input.enableDashboard,
});
return true;
}),
@@ -312,4 +313,37 @@ export const settingsRouter = createTRPCRouter({
return openApiDocument;
},
),
readTraefikEnv: adminProcedure.query(async () => {
const { stdout } = await execAsync(
"docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik",
);
return stdout.trim();
}),
writeTraefikEnv: adminProcedure
.input(z.string())
.mutation(async ({ input }) => {
const envs = prepareEnvironmentVariables(input);
await initializeTraefik({
env: envs,
});
return true;
}),
haveTraefikDashboardPortEnabled: adminProcedure.query(async () => {
const { stdout } = await execAsync(
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
);
const parsed: any[] = JSON.parse(stdout.trim());
for (const port of parsed) {
if (port.PublishedPort === 8080) {
return true;
}
}
return false;
}),
});

View File

@@ -34,21 +34,23 @@ export const sshRouter = createTRPCRouter({
});
}
}),
remove: adminProcedure.input(apiRemoveSshKey).mutation(async ({ input }) => {
try {
return await removeSSHKeyById(input.sshKeyId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this ssh key",
});
}
}),
remove: protectedProcedure
.input(apiRemoveSshKey)
.mutation(async ({ input }) => {
try {
return await removeSSHKeyById(input.sshKeyId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this ssh key",
});
}
}),
one: protectedProcedure.input(apiFindOneSshKey).query(async ({ input }) => {
const sshKey = await findSSHKeyById(input.sshKeyId);
return sshKey;
}),
all: adminProcedure.query(async () => {
all: protectedProcedure.query(async () => {
return await db.query.sshKeys.findMany({});
}),
generate: protectedProcedure
@@ -56,15 +58,17 @@ export const sshRouter = createTRPCRouter({
.mutation(async ({ input }) => {
return await generateSSHKey(input.type);
}),
update: adminProcedure.input(apiUpdateSshKey).mutation(async ({ input }) => {
try {
return await updateSSHKeyById(input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update this ssh key",
cause: error,
});
}
}),
update: protectedProcedure
.input(apiUpdateSshKey)
.mutation(async ({ input }) => {
try {
return await updateSSHKeyById(input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update this ssh key",
cause: error,
});
}
}),
});

View File

@@ -4,6 +4,7 @@ import { db } from "@/server/db";
import { type apiCreateCompose, compose } from "@/server/db/schema";
import { generateAppName } from "@/server/db/schema/utils";
import { buildCompose } from "@/server/utils/builders/compose";
import { cloneCompose, loadDockerCompose } from "@/server/utils/docker/domain";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
@@ -14,7 +15,6 @@ import { createComposeFile } from "@/server/utils/providers/raw";
import { generatePassword } from "@/templates/utils";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { load } from "js-yaml";
import { findAdmin, getDokployUrl } from "./admin";
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
import { validUniqueServerAppName } from "./project";
@@ -91,6 +91,7 @@ export const findComposeById = async (composeId: string) => {
project: true,
deployments: true,
mounts: true,
domains: true,
},
});
if (!result) {
@@ -102,20 +103,27 @@ export const findComposeById = async (composeId: string) => {
return result;
};
export const loadServices = async (composeId: string) => {
export const loadServices = async (
composeId: string,
type: "fetch" | "cache" = "fetch",
) => {
const compose = await findComposeById(composeId);
// use js-yaml to parse the docker compose file and then extact the services
const composeFile = compose.composeFile;
const composeData = load(composeFile) as ComposeSpecification;
if (type === "fetch") {
await cloneCompose(compose);
}
const composeData = await loadDockerCompose(compose);
if (!composeData?.services) {
return ["All Services"];
throw new TRPCError({
code: "NOT_FOUND",
message: "Services not found",
});
}
const services = Object.keys(composeData.services);
return [...services, "All Services"];
return [...services];
};
export const updateCompose = async (

View File

@@ -15,8 +15,6 @@ export type Domain = typeof domains.$inferSelect;
export const createDomain = async (input: typeof apiCreateDomain._type) => {
await db.transaction(async (tx) => {
const application = await findApplicationById(input.applicationId);
const domain = await tx
.insert(domains)
.values({
@@ -32,52 +30,19 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
});
}
await manageDomain(application, domain);
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
await manageDomain(application, domain);
}
});
};
export const generateDomain = async (
input: typeof apiFindDomainByApplication._type,
) => {
const application = await findApplicationById(input.applicationId);
export const generateTraefikMeDomain = async (appName: string) => {
const admin = await findAdmin();
const domain = await createDomain({
applicationId: application.applicationId,
host: generateRandomDomain({
serverIp: admin.serverIp || "",
projectName: application.appName,
}),
port: 3000,
certificateType: "none",
https: false,
path: "/",
return generateRandomDomain({
serverIp: admin.serverIp || "",
projectName: appName,
});
return domain;
};
export const generateWildcard = async (
input: typeof apiFindDomainByApplication._type,
) => {
const application = await findApplicationById(input.applicationId);
const admin = await findAdmin();
if (!admin.host) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "We need a host to generate a wildcard domain",
});
}
const domain = await createDomain({
applicationId: application.applicationId,
host: generateWildcardDomain(application.appName, admin.host || ""),
port: 3000,
certificateType: "none",
https: false,
path: "/",
});
return domain;
};
export const generateWildcardDomain = (
@@ -114,6 +79,17 @@ export const findDomainsByApplicationId = async (applicationId: string) => {
return domainsArray;
};
export const findDomainsByComposeId = async (composeId: string) => {
const domainsArray = await db.query.domains.findMany({
where: eq(domains.composeId, composeId),
with: {
compose: true,
},
});
return domainsArray;
};
export const updateDomainById = async (
domainId: string,
domainData: Partial<Domain>,

View File

@@ -35,6 +35,7 @@ export const buildType = pgEnum("buildType", [
"heroku_buildpacks",
"paketo_buildpacks",
"nixpacks",
"static",
]);
// TODO: refactor this types
@@ -140,6 +141,7 @@ export const applications = pgTable("application", {
},
),
dockerfile: text("dockerfile"),
dockerContextPath: text("dockerContextPath"),
// Drop
dropBuildPath: text("dropBuildPath"),
// Docker swarm json
@@ -157,6 +159,7 @@ export const applications = pgTable("application", {
.notNull()
.default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"),
publishDirectory: text("publishDirectory"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
@@ -315,7 +318,9 @@ const createSchema = createInsertSchema(applications, {
"heroku_buildpacks",
"paketo_buildpacks",
"nixpacks",
"static",
]),
publishDirectory: z.string().optional(),
owner: z.string(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
@@ -352,8 +357,10 @@ export const apiSaveBuildType = createSchema
applicationId: true,
buildType: true,
dockerfile: true,
dockerContextPath: true,
})
.required();
.required()
.merge(createSchema.pick({ publishDirectory: true }));
export const apiSaveGithubProvider = createSchema
.pick({

View File

@@ -1,11 +1,11 @@
import { sshKeys } from "@/server/db/schema/ssh-key";
import { generatePassword } from "@/templates/utils";
import { relations } from "drizzle-orm";
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { deployments } from "./deployment";
import { domains } from "./domain";
import { mounts } from "./mount";
import { projects } from "./project";
import { applicationStatus } from "./shared";
@@ -72,6 +72,7 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
fields: [compose.customGitSSHKeyId],
references: [sshKeys.sshKeyId],
}),
domains: many(domains),
}));
const createSchema = createInsertSchema(compose, {
@@ -106,6 +107,11 @@ export const apiFindCompose = z.object({
composeId: z.string().min(1),
});
export const apiFetchServices = z.object({
composeId: z.string().min(1),
type: z.enum(["fetch", "cache"]).optional().default("cache"),
});
export const apiUpdateCompose = createSchema.partial().extend({
composeId: z.string(),
composeFile: z.string().optional(),

View File

@@ -1,11 +1,22 @@
import { domain } from "@/server/db/validations";
import { domain } from "@/server/db/validations/domain";
import { relations } from "drizzle-orm";
import { boolean, integer, pgTable, serial, text } from "drizzle-orm/pg-core";
import {
boolean,
integer,
pgEnum,
pgTable,
serial,
text,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application";
import { compose } from "./compose";
import { certificateType } from "./shared";
export const domainType = pgEnum("domainType", ["compose", "application"]);
export const domains = pgTable("domain", {
domainId: text("domainId")
.notNull()
@@ -13,15 +24,21 @@ export const domains = pgTable("domain", {
.$defaultFn(() => nanoid()),
host: text("host").notNull(),
https: boolean("https").notNull().default(false),
port: integer("port").default(80),
port: integer("port").default(3000),
path: text("path").default("/"),
serviceName: text("serviceName"),
domainType: domainType("domainType").default("application"),
uniqueConfigKey: serial("uniqueConfigKey"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
applicationId: text("applicationId")
.notNull()
.references(() => applications.applicationId, { onDelete: "cascade" }),
composeId: text("composeId").references(() => compose.composeId, {
onDelete: "cascade",
}),
applicationId: text("applicationId").references(
() => applications.applicationId,
{ onDelete: "cascade" },
),
certificateType: certificateType("certificateType").notNull().default("none"),
});
@@ -30,6 +47,10 @@ export const domainsRelations = relations(domains, ({ one }) => ({
fields: [domains.applicationId],
references: [applications.applicationId],
}),
compose: one(compose, {
fields: [domains.composeId],
references: [compose.composeId],
}),
}));
const createSchema = createInsertSchema(domains, domain._def.schema.shape);
@@ -41,6 +62,9 @@ export const apiCreateDomain = createSchema.pick({
https: true,
applicationId: true,
certificateType: true,
composeId: true,
serviceName: true,
domainType: true,
});
export const apiFindDomain = createSchema
@@ -53,6 +77,14 @@ export const apiFindDomainByApplication = createSchema.pick({
applicationId: true,
});
export const apiCreateTraefikMeDomain = createSchema.pick({}).extend({
appName: z.string().min(1),
});
export const apiFindDomainByCompose = createSchema.pick({
composeId: true,
});
export const apiUpdateDomain = createSchema
.pick({
host: true,
@@ -60,5 +92,7 @@ export const apiUpdateDomain = createSchema
port: true,
https: true,
certificateType: true,
serviceName: true,
domainType: true,
})
.merge(createSchema.pick({ domainId: true }).required());

View File

@@ -28,6 +28,7 @@ export const users = pgTable("user", {
.notNull()
.$defaultFn(() => new Date().toISOString()),
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
canCreateServices: boolean("canCreateServices").notNull().default(false),
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
@@ -107,6 +108,7 @@ export const apiAssignPermissions = createSchema
canAccessToTraefikFiles: true,
canAccessToDocker: true,
canAccessToAPI: true,
canAccessToSSHKeys: true,
})
.required();

View File

@@ -0,0 +1,46 @@
import { z } from "zod";
export const domain = z
.object({
host: z.string().min(1, { message: "Add a hostname" }),
path: z.string().min(1).optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none"]).optional(),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
});
export const domainCompose = z
.object({
host: z.string().min(1, { message: "Host is required" }),
path: z.string().min(1).optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none"]).optional(),
serviceName: z.string().min(1, { message: "Service name is required" }),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
});

View File

@@ -35,27 +35,3 @@ export const sshKeyUpdate = sshKeyCreate.pick({
export const sshKeyType = z.object({
type: z.enum(["rsa", "ed25519"]).optional(),
});
export const domain = z
.object({
host: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/, {
message: "Invalid hostname",
}),
path: z.string().min(1).optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none"]).optional(),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
});

View File

@@ -36,10 +36,9 @@ export const initializePostgres = async () => {
Ports: [
{
TargetPort: 5432,
...(process.env.NODE_ENV === "development"
? { PublishedPort: 5432 }
: {}),
PublishedPort: process.env.NODE_ENV === "development" ? 5432 : 0,
Protocol: "tcp",
PublishMode: "host",
},
],
},

View File

@@ -33,10 +33,9 @@ export const initializeRedis = async () => {
Ports: [
{
TargetPort: 6379,
...(process.env.NODE_ENV === "development"
? { PublishedPort: 6379 }
: {}),
PublishedPort: process.env.NODE_ENV === "development" ? 6379 : 0,
Protocol: "tcp",
PublishMode: "host",
},
],
},

View File

@@ -11,7 +11,15 @@ const TRAEFIK_SSL_PORT =
Number.parseInt(process.env.TRAEFIK_SSL_PORT ?? "", 10) || 443;
const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT ?? "", 10) || 80;
export const initializeTraefik = async (enableDashboard = false) => {
interface TraefikOptions {
enableDashboard?: boolean;
env?: string[];
}
export const initializeTraefik = async ({
enableDashboard = false,
env = [],
}: TraefikOptions = {}) => {
const imageName = "traefik:v2.5";
const containerName = "dokploy-traefik";
const settings: CreateServiceOptions = {
@@ -19,6 +27,7 @@ export const initializeTraefik = async (enableDashboard = false) => {
TaskTemplate: {
ContainerSpec: {
Image: imageName,
Env: env,
Mounts: [
{
Type: "bind",

Some files were not shown because too many files have changed in this diff Show More