diff --git a/backend/Dockerfile b/backend/Dockerfile index a7ae973..cf34877 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,10 +2,12 @@ FROM node:22-alpine WORKDIR /app +RUN apk add --no-cache curl jq + COPY package*.json ./ COPY prisma ./prisma RUN npm ci COPY . . -CMD ["npm", "run", "start"] +CMD ["sh", "-lc", ". /app/scripts/load-vault-env.sh && npm run start"] diff --git a/backend/scripts/load-vault-env.sh b/backend/scripts/load-vault-env.sh new file mode 100755 index 0000000..086c1e8 --- /dev/null +++ b/backend/scripts/load-vault-env.sh @@ -0,0 +1,60 @@ +#!/bin/sh +set -eu + +log() { + printf '%s\n' "$*" >&2 +} + +VAULT_ENABLED="${VAULT_ENABLED:-auto}" +if [ "$VAULT_ENABLED" = "false" ] || [ "$VAULT_ENABLED" = "0" ]; then + exit 0 +fi + +if [ -z "${VAULT_ADDR:-}" ] || [ -z "${VAULT_TOKEN:-}" ]; then + if [ "$VAULT_ENABLED" = "true" ] || [ "$VAULT_ENABLED" = "1" ]; then + log "Vault bootstrap is required but VAULT_ADDR or VAULT_TOKEN is missing." + exit 1 + fi + exit 0 +fi + +if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + log "Vault bootstrap requires curl and jq." + exit 1 +fi + +VAULT_KV_MOUNT="${VAULT_KV_MOUNT:-secret}" + +load_secret_path() { + path="$1" + source_name="$2" + if [ -z "$path" ]; then + return 0 + fi + + url="${VAULT_ADDR%/}/v1/${VAULT_KV_MOUNT}/data/${path}" + response="$(curl -fsS -H "X-Vault-Token: $VAULT_TOKEN" "$url")" || { + log "Failed to load Vault path ${VAULT_KV_MOUNT}/${path}." + return 1 + } + + encoded_items="$(printf '%s' "$response" | jq -r '.data.data // {} | to_entries[]? | @base64')" + if [ -z "$encoded_items" ]; then + return 0 + fi + + old_ifs="${IFS}" + IFS=' +' + for encoded_item in $encoded_items; do + key="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.key')" + value="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.value | tostring')" + export "$key=$value" + done + IFS="${old_ifs}" + + log "Loaded Vault ${source_name} secrets from ${VAULT_KV_MOUNT}/${path}." +} + +load_secret_path "${VAULT_SHARED_PATH:-}" "shared" +load_secret_path "${VAULT_PROJECT_PATH:-}" "project" diff --git a/backend_worker/Dockerfile b/backend_worker/Dockerfile index b5c8b5e..5a94641 100644 --- a/backend_worker/Dockerfile +++ b/backend_worker/Dockerfile @@ -2,12 +2,15 @@ FROM node:22-alpine WORKDIR /app +RUN apk add --no-cache curl jq + COPY package*.json ./ RUN npm ci COPY src ./src +COPY scripts ./scripts COPY tsconfig.json ./tsconfig.json ENV NODE_ENV=production -CMD ["npm", "run", "start"] +CMD ["sh", "-lc", ". /app/scripts/load-vault-env.sh && npm run start"] diff --git a/backend_worker/scripts/load-vault-env.sh b/backend_worker/scripts/load-vault-env.sh new file mode 100755 index 0000000..086c1e8 --- /dev/null +++ b/backend_worker/scripts/load-vault-env.sh @@ -0,0 +1,60 @@ +#!/bin/sh +set -eu + +log() { + printf '%s\n' "$*" >&2 +} + +VAULT_ENABLED="${VAULT_ENABLED:-auto}" +if [ "$VAULT_ENABLED" = "false" ] || [ "$VAULT_ENABLED" = "0" ]; then + exit 0 +fi + +if [ -z "${VAULT_ADDR:-}" ] || [ -z "${VAULT_TOKEN:-}" ]; then + if [ "$VAULT_ENABLED" = "true" ] || [ "$VAULT_ENABLED" = "1" ]; then + log "Vault bootstrap is required but VAULT_ADDR or VAULT_TOKEN is missing." + exit 1 + fi + exit 0 +fi + +if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + log "Vault bootstrap requires curl and jq." + exit 1 +fi + +VAULT_KV_MOUNT="${VAULT_KV_MOUNT:-secret}" + +load_secret_path() { + path="$1" + source_name="$2" + if [ -z "$path" ]; then + return 0 + fi + + url="${VAULT_ADDR%/}/v1/${VAULT_KV_MOUNT}/data/${path}" + response="$(curl -fsS -H "X-Vault-Token: $VAULT_TOKEN" "$url")" || { + log "Failed to load Vault path ${VAULT_KV_MOUNT}/${path}." + return 1 + } + + encoded_items="$(printf '%s' "$response" | jq -r '.data.data // {} | to_entries[]? | @base64')" + if [ -z "$encoded_items" ]; then + return 0 + fi + + old_ifs="${IFS}" + IFS=' +' + for encoded_item in $encoded_items; do + key="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.key')" + value="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.value | tostring')" + export "$key=$value" + done + IFS="${old_ifs}" + + log "Loaded Vault ${source_name} secrets from ${VAULT_KV_MOUNT}/${path}." +} + +load_secret_path "${VAULT_SHARED_PATH:-}" "shared" +load_secret_path "${VAULT_PROJECT_PATH:-}" "project" diff --git a/deploy-map.toml b/deploy-map.toml index 60dacfd..e786d87 100644 --- a/deploy-map.toml +++ b/deploy-map.toml @@ -7,3 +7,4 @@ backend_worker = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" } telegram_backend = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" } telegram_worker = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" } hatchet = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui", compose_path = "hatchet/docker-compose.yml" } +vault = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" } diff --git a/frontend/Dockerfile b/frontend/Dockerfile index c0d4282..13c5266 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -3,7 +3,7 @@ FROM node:22-bookworm-slim WORKDIR /app/frontend RUN apt-get update -y \ - && apt-get install -y --no-install-recommends openssl ca-certificates \ + && apt-get install -y --no-install-recommends openssl ca-certificates curl jq \ && rm -rf /var/lib/apt/lists/* COPY package*.json ./ @@ -30,4 +30,4 @@ ENV NITRO_PORT=3000 EXPOSE 3000 # Keep schema in sync, then start Nitro production server. -CMD ["bash", "-lc", "npx prisma db push && node .output/server/index.mjs"] +CMD ["sh", "-lc", ". /app/frontend/scripts/load-vault-env.sh && npx prisma db push && node .output/server/index.mjs"] diff --git a/frontend/scripts/load-vault-env.sh b/frontend/scripts/load-vault-env.sh new file mode 100755 index 0000000..086c1e8 --- /dev/null +++ b/frontend/scripts/load-vault-env.sh @@ -0,0 +1,60 @@ +#!/bin/sh +set -eu + +log() { + printf '%s\n' "$*" >&2 +} + +VAULT_ENABLED="${VAULT_ENABLED:-auto}" +if [ "$VAULT_ENABLED" = "false" ] || [ "$VAULT_ENABLED" = "0" ]; then + exit 0 +fi + +if [ -z "${VAULT_ADDR:-}" ] || [ -z "${VAULT_TOKEN:-}" ]; then + if [ "$VAULT_ENABLED" = "true" ] || [ "$VAULT_ENABLED" = "1" ]; then + log "Vault bootstrap is required but VAULT_ADDR or VAULT_TOKEN is missing." + exit 1 + fi + exit 0 +fi + +if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + log "Vault bootstrap requires curl and jq." + exit 1 +fi + +VAULT_KV_MOUNT="${VAULT_KV_MOUNT:-secret}" + +load_secret_path() { + path="$1" + source_name="$2" + if [ -z "$path" ]; then + return 0 + fi + + url="${VAULT_ADDR%/}/v1/${VAULT_KV_MOUNT}/data/${path}" + response="$(curl -fsS -H "X-Vault-Token: $VAULT_TOKEN" "$url")" || { + log "Failed to load Vault path ${VAULT_KV_MOUNT}/${path}." + return 1 + } + + encoded_items="$(printf '%s' "$response" | jq -r '.data.data // {} | to_entries[]? | @base64')" + if [ -z "$encoded_items" ]; then + return 0 + fi + + old_ifs="${IFS}" + IFS=' +' + for encoded_item in $encoded_items; do + key="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.key')" + value="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.value | tostring')" + export "$key=$value" + done + IFS="${old_ifs}" + + log "Loaded Vault ${source_name} secrets from ${VAULT_KV_MOUNT}/${path}." +} + +load_secret_path "${VAULT_SHARED_PATH:-}" "shared" +load_secret_path "${VAULT_PROJECT_PATH:-}" "project" diff --git a/telegram_backend/Dockerfile b/telegram_backend/Dockerfile index 57cea74..0b76174 100644 --- a/telegram_backend/Dockerfile +++ b/telegram_backend/Dockerfile @@ -2,9 +2,11 @@ FROM node:22-alpine WORKDIR /app +RUN apk add --no-cache curl jq + COPY package*.json ./ RUN npm ci COPY . . -CMD ["npm", "run", "start"] +CMD ["sh", "-lc", ". /app/scripts/load-vault-env.sh && npm run start"] diff --git a/telegram_backend/scripts/load-vault-env.sh b/telegram_backend/scripts/load-vault-env.sh new file mode 100755 index 0000000..086c1e8 --- /dev/null +++ b/telegram_backend/scripts/load-vault-env.sh @@ -0,0 +1,60 @@ +#!/bin/sh +set -eu + +log() { + printf '%s\n' "$*" >&2 +} + +VAULT_ENABLED="${VAULT_ENABLED:-auto}" +if [ "$VAULT_ENABLED" = "false" ] || [ "$VAULT_ENABLED" = "0" ]; then + exit 0 +fi + +if [ -z "${VAULT_ADDR:-}" ] || [ -z "${VAULT_TOKEN:-}" ]; then + if [ "$VAULT_ENABLED" = "true" ] || [ "$VAULT_ENABLED" = "1" ]; then + log "Vault bootstrap is required but VAULT_ADDR or VAULT_TOKEN is missing." + exit 1 + fi + exit 0 +fi + +if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + log "Vault bootstrap requires curl and jq." + exit 1 +fi + +VAULT_KV_MOUNT="${VAULT_KV_MOUNT:-secret}" + +load_secret_path() { + path="$1" + source_name="$2" + if [ -z "$path" ]; then + return 0 + fi + + url="${VAULT_ADDR%/}/v1/${VAULT_KV_MOUNT}/data/${path}" + response="$(curl -fsS -H "X-Vault-Token: $VAULT_TOKEN" "$url")" || { + log "Failed to load Vault path ${VAULT_KV_MOUNT}/${path}." + return 1 + } + + encoded_items="$(printf '%s' "$response" | jq -r '.data.data // {} | to_entries[]? | @base64')" + if [ -z "$encoded_items" ]; then + return 0 + fi + + old_ifs="${IFS}" + IFS=' +' + for encoded_item in $encoded_items; do + key="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.key')" + value="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.value | tostring')" + export "$key=$value" + done + IFS="${old_ifs}" + + log "Loaded Vault ${source_name} secrets from ${VAULT_KV_MOUNT}/${path}." +} + +load_secret_path "${VAULT_SHARED_PATH:-}" "shared" +load_secret_path "${VAULT_PROJECT_PATH:-}" "project" diff --git a/telegram_worker/Dockerfile b/telegram_worker/Dockerfile index b5c8b5e..5a94641 100644 --- a/telegram_worker/Dockerfile +++ b/telegram_worker/Dockerfile @@ -2,12 +2,15 @@ FROM node:22-alpine WORKDIR /app +RUN apk add --no-cache curl jq + COPY package*.json ./ RUN npm ci COPY src ./src +COPY scripts ./scripts COPY tsconfig.json ./tsconfig.json ENV NODE_ENV=production -CMD ["npm", "run", "start"] +CMD ["sh", "-lc", ". /app/scripts/load-vault-env.sh && npm run start"] diff --git a/telegram_worker/scripts/load-vault-env.sh b/telegram_worker/scripts/load-vault-env.sh new file mode 100755 index 0000000..086c1e8 --- /dev/null +++ b/telegram_worker/scripts/load-vault-env.sh @@ -0,0 +1,60 @@ +#!/bin/sh +set -eu + +log() { + printf '%s\n' "$*" >&2 +} + +VAULT_ENABLED="${VAULT_ENABLED:-auto}" +if [ "$VAULT_ENABLED" = "false" ] || [ "$VAULT_ENABLED" = "0" ]; then + exit 0 +fi + +if [ -z "${VAULT_ADDR:-}" ] || [ -z "${VAULT_TOKEN:-}" ]; then + if [ "$VAULT_ENABLED" = "true" ] || [ "$VAULT_ENABLED" = "1" ]; then + log "Vault bootstrap is required but VAULT_ADDR or VAULT_TOKEN is missing." + exit 1 + fi + exit 0 +fi + +if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + log "Vault bootstrap requires curl and jq." + exit 1 +fi + +VAULT_KV_MOUNT="${VAULT_KV_MOUNT:-secret}" + +load_secret_path() { + path="$1" + source_name="$2" + if [ -z "$path" ]; then + return 0 + fi + + url="${VAULT_ADDR%/}/v1/${VAULT_KV_MOUNT}/data/${path}" + response="$(curl -fsS -H "X-Vault-Token: $VAULT_TOKEN" "$url")" || { + log "Failed to load Vault path ${VAULT_KV_MOUNT}/${path}." + return 1 + } + + encoded_items="$(printf '%s' "$response" | jq -r '.data.data // {} | to_entries[]? | @base64')" + if [ -z "$encoded_items" ]; then + return 0 + fi + + old_ifs="${IFS}" + IFS=' +' + for encoded_item in $encoded_items; do + key="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.key')" + value="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.value | tostring')" + export "$key=$value" + done + IFS="${old_ifs}" + + log "Loaded Vault ${source_name} secrets from ${VAULT_KV_MOUNT}/${path}." +} + +load_secret_path "${VAULT_SHARED_PATH:-}" "shared" +load_secret_path "${VAULT_PROJECT_PATH:-}" "project" diff --git a/vault/.dockerignore b/vault/.dockerignore new file mode 100644 index 0000000..ed26872 --- /dev/null +++ b/vault/.dockerignore @@ -0,0 +1,2 @@ +backups/ +*.json diff --git a/vault/Dockerfile b/vault/Dockerfile new file mode 100644 index 0000000..c928e89 --- /dev/null +++ b/vault/Dockerfile @@ -0,0 +1,7 @@ +FROM hashicorp/vault:1.21.3 + +COPY config /vault/config + +EXPOSE 8200 8201 + +CMD ["vault", "server", "-config=/vault/config/vault.hcl"] diff --git a/vault/README.md b/vault/README.md new file mode 100644 index 0000000..b1d73df --- /dev/null +++ b/vault/README.md @@ -0,0 +1,31 @@ +# Vault Setup + +This folder is intended for Dokploy deployment via `Dockerfile` (not docker-compose). + +## Build/Run + +Container image uses `vault/config/vault.hcl` and starts: + +```bash +vault server -config=/vault/config/vault.hcl +``` + +Required runtime settings in Dokploy: + +- add capability: `IPC_LOCK` +- mount persistent volume to `/vault/data` +- expose port `8200` (API) +- optionally expose `8201` (cluster) + +## KV Layout + +Vault stores environment variables in KV v2 under: + +- `secret/shared/` +- `secret/projects//` + +Examples: + +- `secret/shared/prod` +- `secret/projects/backend/prod` +- `secret/projects/frontend/prod` diff --git a/vault/config/vault.hcl b/vault/config/vault.hcl new file mode 100644 index 0000000..654ea92 --- /dev/null +++ b/vault/config/vault.hcl @@ -0,0 +1,16 @@ +ui = true +disable_mlock = true + +storage "raft" { + path = "/vault/data" + node_id = "vault-1" +} + +listener "tcp" { + address = "0.0.0.0:8200" + cluster_address = "0.0.0.0:8201" + tls_disable = 1 +} + +cluster_addr = "http://127.0.0.1:8201" +api_addr = "http://0.0.0.0:8200"