upd
This commit is contained in:
parent
6a5820dda1
commit
5ea42bb24b
21 changed files with 160 additions and 122 deletions
|
|
@ -36,6 +36,9 @@ type Config struct {
|
|||
RootUser string
|
||||
Password string
|
||||
}
|
||||
GOFS struct {
|
||||
Endpoint string
|
||||
}
|
||||
}
|
||||
|
||||
Server struct {
|
||||
|
|
@ -79,7 +82,8 @@ func Load() *Config {
|
|||
Cfg.Storage.TempLimit = getEnvAsInt("TEMP_STORAGE_LIMIT", 5)
|
||||
strategy := getEnv("STORAGE_STRATEGY", "fs")
|
||||
|
||||
if strategy == "s3" || strategy == "minio" {
|
||||
switch strategy {
|
||||
case "s3", "minio":
|
||||
Cfg.Storage.Minio.URL = getEnv("MINIO_URL", "")
|
||||
Cfg.Storage.Minio.RootUser = getEnv("MINIO_ROOT_USER", "")
|
||||
Cfg.Storage.Minio.Password = getEnv("MINIO_ROOT_PASSWORD", "")
|
||||
|
|
@ -90,7 +94,10 @@ func Load() *Config {
|
|||
} else {
|
||||
Cfg.Storage.Strategy = typesdefs.S3
|
||||
}
|
||||
} else {
|
||||
case "gofs":
|
||||
Cfg.Storage.GOFS.Endpoint = getEnv("GOFS_ENDPOINT", "localhost:8080")
|
||||
Cfg.Storage.Strategy = typesdefs.GOFS
|
||||
default:
|
||||
Cfg.Storage.Strategy = typesdefs.FS
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import (
|
|||
"path/filepath"
|
||||
)
|
||||
|
||||
var _ StorageHandler = (*Fs_Handler)(nil)
|
||||
|
||||
type Fs_Handler struct {
|
||||
}
|
||||
|
||||
|
|
|
|||
93
apps/backend/storage_handlers/go_fs.go
Normal file
93
apps/backend/storage_handlers/go_fs.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package storagehandlers
|
||||
|
||||
import (
|
||||
"backend/config"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var _ StorageHandler = (*GOFS_Handler)(nil)
|
||||
|
||||
type GOFS_Handler struct {
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
func NewGoFsHandler() *GOFS_Handler {
|
||||
|
||||
return &GOFS_Handler{
|
||||
Endpoint: config.Cfg.Storage.GOFS.Endpoint,
|
||||
}
|
||||
}
|
||||
func (g *GOFS_Handler) Init() error {
|
||||
resp, err := http.Get(g.Endpoint + "/health")
|
||||
if err != nil {
|
||||
return fmt.Errorf("storage server unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("storage server unhealthy: %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (g *GOFS_Handler) Add(file *multipart.FileHeader, fileId string) error {
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", file.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(part, f); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = writer.WriteField("fileId", fileId)
|
||||
writer.Close()
|
||||
|
||||
req, err := http.NewRequest("POST", g.Endpoint+"/upload", body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("upload failed: %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GOFS_Handler) Delete(fileId string) error {
|
||||
req, err := http.NewRequest("DELETE", g.Endpoint+"/delete/"+fileId, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return fmt.Errorf("delete failed: %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GOFS_Handler) Get(filename string) (string, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (g *GOFS_Handler) List() ([]string, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ var (
|
|||
useSSL = false
|
||||
bucketName = "storage"
|
||||
)
|
||||
var _ StorageHandler = (*S3_Handler)(nil)
|
||||
|
||||
type S3_Handler struct {
|
||||
minioClient *minio.Client
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ func NewStorageHandler(strategy typesdefs.StorageStrategy) (StorageHandler, erro
|
|||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
case typesdefs.GOFS:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, errors.New("invalid storage strategy")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package typesdefs
|
|||
type StorageStrategy string
|
||||
|
||||
const (
|
||||
FS StorageStrategy = "fs"
|
||||
S3 StorageStrategy = "s3"
|
||||
FS StorageStrategy = "fs"
|
||||
S3 StorageStrategy = "s3"
|
||||
GOFS StorageStrategy = "gofs"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ ENV TILES_URL="localhost:6379"
|
|||
ENV SKEBBY_USER="mock"
|
||||
ENV SKEBBY_PASS="mock"
|
||||
ENV REVALIDATION_SECRET="mock"
|
||||
ENV STORAGE_TOKEN="mock"
|
||||
|
||||
|
||||
RUN SKIP_ENV_VALIDATION=1 npm run build
|
||||
|
|
@ -131,6 +132,8 @@ ENV SKEBBY_USER=$SKEBBY_USER
|
|||
ARG SKEBBY_PASS
|
||||
ENV SKEBBY_PASS=$SKEBBY_PASS
|
||||
ENV REVALIDATION_SECRET="SWKpgaaLfsyeqV1eOT0WG7TUFBewir8kJXure3O37ki8Lt4Z4IiEgBW4zPHDdM5c"
|
||||
ARG STORAGE_TOKEN
|
||||
ENV STORAGE_TOKEN=$STORAGE_TOKEN
|
||||
|
||||
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
* This is especially useful for Docker builds.
|
||||
*/
|
||||
|
||||
import { env } from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { NextConfig } from "next";
|
||||
import { env } from "~/env";
|
||||
|
||||
async function createNextConfig(): Promise<NextConfig> {
|
||||
const { createJiti } = await import("jiti");
|
||||
|
|
@ -60,8 +60,8 @@ async function createNextConfig(): Promise<NextConfig> {
|
|||
rewrites: async () => {
|
||||
return [
|
||||
{
|
||||
source: "/storage/:slug*",
|
||||
destination: "http://localhost:8080/:slug*",
|
||||
source: "/storage-api/:slug*",
|
||||
destination: `http://localhost:8080/:slug*?token=${env.STORAGE_TOKEN}`,
|
||||
},
|
||||
{
|
||||
destination: env.NODE_ENV === "production" ? "/404" : "/api/panel",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export const env = createEnv({
|
|||
SKEBBY_USER: z.string(),
|
||||
SKEBBY_PASS: z.string(),
|
||||
REVALIDATION_SECRET: z.string(),
|
||||
STORAGE_TOKEN: z.string(),
|
||||
},
|
||||
client: {
|
||||
NEXT_PUBLIC_BASE_URL: z.string(),
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { env } from "~/env";
|
||||
|
||||
export const apisMiddleware = async (req: NextRequest) => {
|
||||
const path = req.nextUrl.pathname;
|
||||
|
||||
const rewrite_paths = ["/go-api/images/", "/go-api/storage/"];
|
||||
if (!rewrite_paths.some((p) => path.startsWith(p))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
path.startsWith("/go-api/images/") ||
|
||||
path.startsWith("/go-api/storage/")
|
||||
) {
|
||||
// Handle image and storage requests
|
||||
const newPath = path
|
||||
.replace("/go-api/images/", "/images/")
|
||||
.replace("/go-api/storage/", "/storage/");
|
||||
|
||||
const url = new URL(`${env.BACKENDSERVER_URL}${newPath}`, req.url);
|
||||
const res = NextResponse.rewrite(url);
|
||||
res.headers.set("Access-Control-Allow-Origin", env.NEXT_PUBLIC_BASE_URL);
|
||||
|
||||
res.headers.set(
|
||||
"Access-Control-Allow-Methods",
|
||||
"GET, POST, PUT, DELETE, OPTIONS",
|
||||
);
|
||||
res.headers.set(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Content-Type, Authorization",
|
||||
);
|
||||
return res;
|
||||
|
||||
// Fetch the content from the backend and serve it through the middleware
|
||||
/*
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
return new NextResponse(response.body, {
|
||||
headers: response.headers,
|
||||
status: response.status,
|
||||
});*/
|
||||
}
|
||||
};
|
||||
|
|
@ -243,7 +243,7 @@ function FileManagementPage() {
|
|||
<img
|
||||
alt={file.originalName}
|
||||
className="size-20"
|
||||
src={`/storage/file/${file.id}`}
|
||||
src={`/storage-api/file/${file.id}`}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
|
|
@ -260,7 +260,7 @@ function FileManagementPage() {
|
|||
<div className="flex space-x-2">
|
||||
<a
|
||||
className="rounded bg-blue-500 px-3 py-1 text-sm text-white"
|
||||
href={`/storage/file/${file.id}`}
|
||||
href={`/storage-api/file/${file.id}`}
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ import { db } from "~/server/db";
|
|||
import { RateLimiterHandler } from "~/server/services/ratelimiter";
|
||||
import {
|
||||
addUserStorageJunc,
|
||||
deleteFile,
|
||||
deleteHandler,
|
||||
deleteUserStorageJunc,
|
||||
getNewTokenHandler,
|
||||
updateFile,
|
||||
updateHandler,
|
||||
} from "~/server/services/storage.service";
|
||||
import { zStorageIndexId, zUserId } from "~/server/utils/zod_types";
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ export const storageRouter = createTRPCRouter({
|
|||
deleteStorage: protectedProcedure
|
||||
.input(z.object({ storageId: zStorageIndexId }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await deleteFile({
|
||||
return await deleteHandler({
|
||||
cb: async (fileId) => {
|
||||
await db
|
||||
.deleteFrom("storageindex")
|
||||
|
|
@ -133,7 +133,7 @@ export const storageRouter = createTRPCRouter({
|
|||
renameFile: adminProcedure
|
||||
.input(z.object({ fileId: zStorageIndexId, name: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await updateFile({
|
||||
return await updateHandler({
|
||||
data: { filename: input.name },
|
||||
db,
|
||||
fileId: input.fileId,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type { UsersId } from "~/schemas/public/Users";
|
|||
import type { Querier } from "~/server/db";
|
||||
import {
|
||||
addUserStorageJunc,
|
||||
deleteFile,
|
||||
deleteHandler,
|
||||
deleteUserStorageJunc,
|
||||
} from "~/server/services/storage.service";
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ export const deleteUserStorageHandler = async ({
|
|||
return "no file";
|
||||
}
|
||||
|
||||
await deleteFile({
|
||||
await deleteHandler({
|
||||
cb: async (fileId) => {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await deleteUserStorageJunc({
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
import type { TempTokensToken } from "~/schemas/public/TempTokens";
|
||||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import type { Querier } from "~/server/db";
|
||||
import { deleteFile } from "../storage";
|
||||
|
||||
export const getNewTokenHandler = async ({
|
||||
db,
|
||||
|
|
@ -43,7 +44,7 @@ const newToken = async ({ db }: { db: Querier }) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const deleteFile = async ({
|
||||
export const deleteHandler = async ({
|
||||
db,
|
||||
fileId,
|
||||
cb,
|
||||
|
|
@ -58,22 +59,8 @@ export const deleteFile = async ({
|
|||
.select(["id", "ext"])
|
||||
.where("id", "=", fileId)
|
||||
.executeTakeFirstOrThrow();
|
||||
await deleteFile(storage.id);
|
||||
|
||||
const authToken = (await getNewTokenHandler({ db }))[0];
|
||||
if (!authToken) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Error while getting auth token",
|
||||
});
|
||||
}
|
||||
const url = `${env.BACKENDSERVER_URL}/storage/delete/${storage.id}${storage.ext}?token=${authToken}`;
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("File remove failed");
|
||||
}
|
||||
if (cb) {
|
||||
await cb(fileId);
|
||||
}
|
||||
|
|
@ -86,32 +73,7 @@ export const deleteFile = async ({
|
|||
}
|
||||
};
|
||||
|
||||
// export const deleteFiles = async ({
|
||||
// db,
|
||||
// refs,
|
||||
// cb,
|
||||
// }: {
|
||||
// db: Querier;
|
||||
// refs: { storageid: StorageindexId }[];
|
||||
// cb?: (refs: { storageid: StorageindexId }[]) => Promise<void>;
|
||||
// }) => {
|
||||
// try {
|
||||
// await Promise.all(
|
||||
// refs.map((ref) => deleteFile({ db, fileId: ref.storageid })),
|
||||
// );
|
||||
// if (cb) {
|
||||
// await cb(refs);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error("Error deleting files:", error);
|
||||
// throw new TRPCError({
|
||||
// code: "INTERNAL_SERVER_ERROR",
|
||||
// message: "Error while deleting files",
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
export const updateFile = async ({
|
||||
export const updateHandler = async ({
|
||||
db,
|
||||
fileId,
|
||||
data,
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ export type FileMetadata = z.infer<typeof FileMetadataSchema>;
|
|||
// Helper to fetch the list of all files
|
||||
export async function fetchFiles(): Promise<FileMetadata[]> {
|
||||
try {
|
||||
const response = await fetch(`/storage/api/files`);
|
||||
const response = await fetch(`/storage-api/api/files`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch files: ${response.statusText}`);
|
||||
console.error("Failed to fetch file list:", response.statusText);
|
||||
return [];
|
||||
}
|
||||
const parse = FileMetadataSchema.array().safeParse(await response.json());
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ export async function uploadFile(
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/storage/upload`, {
|
||||
const response = await fetch(`/storage-api/upload`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
|
@ -66,17 +67,10 @@ export async function uploadFile(
|
|||
|
||||
// Helper to delete a file
|
||||
export async function deleteFile(fileID: string): Promise<boolean> {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append("id", fileID);
|
||||
|
||||
try {
|
||||
// The Go server accepts POST with form data for delete
|
||||
const response = await fetch(`/storage/delete`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: formData.toString(),
|
||||
const response = await fetch(`/storage-api/delete/${fileID}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
// Treat redirect as success
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import type { AppRouter } from "~/server/api/root";
|
|||
const getUrl = () => {
|
||||
const base = (() => {
|
||||
if (typeof window !== "undefined") return window.location.origin;
|
||||
if (process.env.APP_URL) return process.env.APP_URL;
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ set -euo pipefail
|
|||
|
||||
: "${TAG:?TAG is required}"
|
||||
: "${IMAGE_REGISTRY:?IMAGE_REGISTRY is required}"
|
||||
: "${PREFIX:?PREFIX is required}"
|
||||
: "${OWNER:?OWNER is required}"
|
||||
: "${SERVICES:?SERVICES is required}"
|
||||
: "${USERNAME:?USERNAME is required}"
|
||||
|
|
@ -55,12 +56,12 @@ for svc in ${SERVICES}; do
|
|||
;;
|
||||
esac
|
||||
|
||||
IMAGE="${IMAGE_REGISTRY}/${OWNER}/infoalloggi-${svc}:${TAG}"
|
||||
echo "Building ${svc} -> ${IMAGE} (context: ${ctx})"
|
||||
IMAGE="${IMAGE_REGISTRY}/${OWNER}/${PREFIX}-${svc}:${TAG}"
|
||||
echo "Building ${PREFIX}-${svc} -> ${IMAGE} (context: ${ctx})"
|
||||
docker build --build-arg ENV_FILE="${ENV_FILE}" -t "${IMAGE}" -f "$ctx/$dockerfile" "$ctx"
|
||||
|
||||
echo "Pushing ${IMAGE}"
|
||||
docker push "${IMAGE}"
|
||||
done
|
||||
|
||||
echo "All done. Built and pushed services: ${SERVICES} with tag ${TAG} to ${IMAGE_REGISTRY}/${OWNER}/infoalloggi-<service>:${TAG}"
|
||||
echo "All done. Built and pushed services: ${SERVICES} with tag ${TAG} to ${IMAGE_REGISTRY}/${OWNER}/${PREFIX}-<service>:${TAG}"
|
||||
|
|
|
|||
14
deploy_infoalloggi.sh
Normal file
14
deploy_infoalloggi.sh
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Preset for staging environment
|
||||
export TAG="latest"
|
||||
export IMAGE_REGISTRY="ghcr.io"
|
||||
export OWNER="marcopedone"
|
||||
export SERVICES="web backend db"
|
||||
export USERNAME="marcopedone"
|
||||
export TOKEN="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"
|
||||
export ENV_FILE=".env.infoalloggi"
|
||||
export PREFIX="infoalloggi"
|
||||
|
||||
./build_and_push_ghcr.sh
|
||||
|
|
@ -9,5 +9,6 @@ export SERVICES="web backend db"
|
|||
export USERNAME="marcopedone"
|
||||
export TOKEN="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"
|
||||
export ENV_FILE=".env.marcopedone"
|
||||
export PREFIX="marcopedone"
|
||||
|
||||
./build_and_push_ghcr.sh
|
||||
|
|
@ -150,6 +150,7 @@ services:
|
|||
JWT_SECRET: ${JWT_SECRET}
|
||||
SKEBBY_USER: ${SKEBBY_USER}
|
||||
SKEBBY_PASS: ${SKEBBY_PASS}
|
||||
STORAGE_TOKEN: ${STORAGE_TOKEN}
|
||||
working_dir: /app
|
||||
environment:
|
||||
INTERNAL_BASE_URL: http://web:3000
|
||||
|
|
@ -176,6 +177,7 @@ services:
|
|||
JWT_SECRET: ${JWT_SECRET}
|
||||
SKEBBY_USER: ${SKEBBY_USER}
|
||||
SKEBBY_PASS: ${SKEBBY_PASS}
|
||||
STORAGE_TOKEN: ${STORAGE_TOKEN}
|
||||
networks:
|
||||
- dokploy-network
|
||||
ports:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
db:
|
||||
image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/infoalloggi-db:${TAG:-latest}"
|
||||
image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-db:${TAG:-latest}"
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
|
@ -78,7 +78,7 @@ services:
|
|||
cpus: "0.5"
|
||||
|
||||
backend:
|
||||
image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/infoalloggi-backend:${TAG:-latest}"
|
||||
image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-backend:${TAG:-latest}"
|
||||
networks:
|
||||
- dokploy-network
|
||||
expose:
|
||||
|
|
@ -116,7 +116,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
web:
|
||||
image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/infoalloggi-web:${TAG:-latest}"
|
||||
image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-web:${TAG:-latest}"
|
||||
working_dir: /app
|
||||
pull_policy: "always"
|
||||
environment:
|
||||
|
|
@ -145,6 +145,7 @@ services:
|
|||
JWT_SECRET: ${JWT_SECRET}
|
||||
SKEBBY_USER: ${SKEBBY_USER}
|
||||
SKEBBY_PASS: ${SKEBBY_PASS}
|
||||
STORAGE_TOKEN: ${STORAGE_TOKEN}
|
||||
networks:
|
||||
- dokploy-network
|
||||
ports:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue