This commit is contained in:
parent
26c3c0bb0e
commit
222ad13724
@ -10,21 +10,68 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
COMPOSE_PROJECT_NAME: voxblog
|
COMPOSE_PROJECT_NAME: voxblog
|
||||||
|
INFISICAL_TOKEN: ${{ secrets.INFISICAL_TOKEN }}
|
||||||
|
INFISICAL_SITE_URL: ${{ secrets.INFISICAL_SITE_URL }}
|
||||||
|
INFISICAL_CLI_IMAGE: infisical/cli:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Create placeholder .env
|
||||||
|
run: touch .env
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Ensure .env file exists
|
- name: Load secrets from Infisical
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ ! -f .env ]; then
|
set -euo pipefail
|
||||||
echo ".env file is missing. Add it to the repository or provision it before deployment."
|
|
||||||
|
if [ -z "${INFISICAL_TOKEN}" ]; then
|
||||||
|
echo "INFISICAL_TOKEN is not configured"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
CLI_IMAGE="${INFISICAL_CLI_IMAGE:-infisical/cli:latest}"
|
||||||
|
docker pull "$CLI_IMAGE" >/dev/null
|
||||||
|
|
||||||
|
tmp_file=$(mktemp)
|
||||||
|
if [ -n "${INFISICAL_API_URL:-}" ]; then
|
||||||
|
docker run --rm \
|
||||||
|
-e INFISICAL_TOKEN="$INFISICAL_TOKEN" \
|
||||||
|
${INFISICAL_SITE_URL:+-e INFISICAL_SITE_URL="$INFISICAL_SITE_URL"} \
|
||||||
|
-e INFISICAL_API_URL="$INFISICAL_API_URL" \
|
||||||
|
"$CLI_IMAGE" export --format=dotenv > "$tmp_file"
|
||||||
|
elif [ -n "${INFISICAL_SITE_URL:-}" ]; then
|
||||||
|
api_url="${INFISICAL_SITE_URL%/}/api"
|
||||||
|
docker run --rm \
|
||||||
|
-e INFISICAL_TOKEN="$INFISICAL_TOKEN" \
|
||||||
|
-e INFISICAL_SITE_URL="$INFISICAL_SITE_URL" \
|
||||||
|
-e INFISICAL_API_URL="$api_url" \
|
||||||
|
"$CLI_IMAGE" export --format=dotenv > "$tmp_file"
|
||||||
|
else
|
||||||
|
docker run --rm \
|
||||||
|
-e INFISICAL_TOKEN="$INFISICAL_TOKEN" \
|
||||||
|
"$CLI_IMAGE" export --format=dotenv > "$tmp_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Persist a runtime .env so external checks that expect the file succeed.
|
||||||
|
cp "$tmp_file" .env
|
||||||
|
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
if [ -z "$line" ] || [[ "$line" == \#* ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
key="${line%%=*}"
|
||||||
|
value="${line#*=}"
|
||||||
|
echo "::add-mask::$value"
|
||||||
|
printf '%s=%s\n' "$key" "$value" >> "$GITHUB_ENV"
|
||||||
|
done < "$tmp_file"
|
||||||
|
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
|
||||||
- name: Stop existing containers
|
- name: Stop existing containers
|
||||||
run: docker-compose down || true
|
run: docker-compose down || true
|
||||||
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,8 +7,8 @@ node_modules/
|
|||||||
/packages/**/dist/
|
/packages/**/dist/
|
||||||
|
|
||||||
# Env & secrets
|
# Env & secrets
|
||||||
.env.local
|
|
||||||
.env
|
.env
|
||||||
|
.env.local
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
apps/**/.env
|
apps/**/.env
|
||||||
|
|||||||
@ -267,34 +267,66 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
COMPOSE_PROJECT_NAME: voxblog
|
COMPOSE_PROJECT_NAME: voxblog
|
||||||
|
INFISICAL_TOKEN: ${{ secrets.INFISICAL_TOKEN }}
|
||||||
|
INFISICAL_SITE_URL: ${{ secrets.INFISICAL_SITE_URL }}
|
||||||
|
INFISICAL_CLI_IMAGE: infisical/cli:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Create placeholder .env
|
||||||
|
run: touch .env
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Create .env file
|
- name: Load secrets from Infisical
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cat > .env << EOF
|
set -euo pipefail
|
||||||
DB_ROOT_PASSWORD=${{ secrets.DB_ROOT_PASSWORD }}
|
|
||||||
DB_PASSWORD=${{ secrets.DB_PASSWORD }}
|
if [ -z "${INFISICAL_TOKEN}" ]; then
|
||||||
DB_USER=${{ secrets.DB_USER }}
|
echo "INFISICAL_TOKEN is not configured"
|
||||||
DB_NAME=${{ secrets.DB_NAME }}
|
exit 1
|
||||||
DB_HOST=${{ secrets.DB_HOST }}
|
fi
|
||||||
DB_PORT=${{ secrets.DB_PORT }}
|
|
||||||
ADMIN_PASSWORD=${{ secrets.ADMIN_PASSWORD }}
|
CLI_IMAGE="${INFISICAL_CLI_IMAGE:-infisical/cli:latest}"
|
||||||
OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}
|
docker pull "$CLI_IMAGE" >/dev/null
|
||||||
GHOST_ADMIN_API_KEY=${{ secrets.GHOST_ADMIN_API_KEY }}
|
|
||||||
GHOST_ADMIN_API_URL=${{ secrets.GHOST_ADMIN_API_URL }}
|
tmp_file=$(mktemp)
|
||||||
S3_BUCKET=${{ secrets.S3_BUCKET }}
|
if [ -n "${INFISICAL_API_URL:-}" ]; then
|
||||||
S3_REGION=${{ secrets.S3_REGION }}
|
docker run --rm \
|
||||||
S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY }}
|
-e INFISICAL_TOKEN="$INFISICAL_TOKEN" \
|
||||||
S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY }}
|
${INFISICAL_SITE_URL:+-e INFISICAL_SITE_URL="$INFISICAL_SITE_URL"} \
|
||||||
S3_ENDPOINT=${{ secrets.S3_ENDPOINT }}
|
-e INFISICAL_API_URL="$INFISICAL_API_URL" \
|
||||||
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
"$CLI_IMAGE" export --format=dotenv > "$tmp_file"
|
||||||
EOF
|
elif [ -n "${INFISICAL_SITE_URL:-}" ]; then
|
||||||
|
api_url="${INFISICAL_SITE_URL%/}/api"
|
||||||
|
docker run --rm \
|
||||||
|
-e INFISICAL_TOKEN="$INFISICAL_TOKEN" \
|
||||||
|
-e INFISICAL_SITE_URL="$INFISICAL_SITE_URL" \
|
||||||
|
-e INFISICAL_API_URL="$api_url" \
|
||||||
|
"$CLI_IMAGE" export --format=dotenv > "$tmp_file"
|
||||||
|
else
|
||||||
|
docker run --rm \
|
||||||
|
-e INFISICAL_TOKEN="$INFISICAL_TOKEN" \
|
||||||
|
"$CLI_IMAGE" export --format=dotenv > "$tmp_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$tmp_file" .env
|
||||||
|
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
if [ -z "$line" ] || [[ "$line" == \#* ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
key="${line%%=*}"
|
||||||
|
value="${line#*=}"
|
||||||
|
echo "::add-mask::$value"
|
||||||
|
printf '%s=%s\n' "$key" "$value" >> "$GITHUB_ENV"
|
||||||
|
done < "$tmp_file"
|
||||||
|
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
|
||||||
- name: Build and deploy
|
- name: Build and deploy
|
||||||
run: |
|
run: |
|
||||||
@ -319,73 +351,21 @@ jobs:
|
|||||||
|
|
||||||
## Step 4: Deployment Script (Alternative to Gitea Actions)
|
## Step 4: Deployment Script (Alternative to Gitea Actions)
|
||||||
|
|
||||||
If Gitea Actions is not available, use a webhook + script approach:
|
If Gitea Actions is not available, you can still trigger deployments by SSH, cron, or a webhook that calls `deploy.sh`. The script now:
|
||||||
|
|
||||||
|
- Prefers `INFISICAL_TOKEN` and (optionally) `INFISICAL_SITE_URL` to pull secrets from Infisical via the official CLI container.
|
||||||
|
- Falls back to a local `.env` file only when no token is exported (for development/testing).
|
||||||
|
- Runs the same build → up → migrate → health-check flow afterwards.
|
||||||
|
|
||||||
|
Before running it manually or from a webhook, export the token:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
export INFISICAL_TOKEN=st.your_service_token
|
||||||
# deploy.sh
|
export INFISICAL_SITE_URL=https://secrets.yourdomain.com # optional
|
||||||
|
./deploy.sh
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🚀 Starting deployment..."
|
|
||||||
|
|
||||||
# Pull latest code
|
|
||||||
echo "📥 Pulling latest code..."
|
|
||||||
git pull origin main
|
|
||||||
|
|
||||||
# Create .env if not exists
|
|
||||||
if [ ! -f .env ]; then
|
|
||||||
echo "⚠️ .env file not found! Please create it from .env.example"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stop existing containers
|
|
||||||
echo "🛑 Stopping existing containers..."
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Build new images
|
|
||||||
echo "🔨 Building new images..."
|
|
||||||
docker-compose build --no-cache
|
|
||||||
|
|
||||||
# Start containers
|
|
||||||
echo "▶️ Starting containers..."
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Wait for services to be ready
|
|
||||||
echo "⏳ Waiting for services..."
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# Run migrations
|
|
||||||
echo "🗄️ Running database migrations..."
|
|
||||||
docker-compose exec -T api pnpm run drizzle:migrate
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
echo "🏥 Health check..."
|
|
||||||
if curl -f http://localhost:3301/api/health; then
|
|
||||||
echo "✅ API is healthy"
|
|
||||||
else
|
|
||||||
echo "❌ API health check failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if curl -f http://localhost:3300; then
|
|
||||||
echo "✅ Admin is healthy"
|
|
||||||
else
|
|
||||||
echo "❌ Admin health check failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
echo "🧹 Cleaning up old images..."
|
|
||||||
docker image prune -af --filter "until=24h"
|
|
||||||
|
|
||||||
echo "✅ Deployment complete!"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Make it executable:
|
Everything else inside the script remains unchanged; see [deploy.sh](deploy.sh) for details.
|
||||||
```bash
|
|
||||||
chmod +x deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 5: Gitea Webhook Setup
|
## Step 5: Gitea Webhook Setup
|
||||||
|
|
||||||
@ -548,14 +528,11 @@ sudo certbot --nginx -d voxblog.yourdomain.com
|
|||||||
|
|
||||||
## Step 8: Environment Variables
|
## Step 8: Environment Variables
|
||||||
|
|
||||||
Create `.env` on your VPS:
|
Provision secrets in Infisical instead of maintaining a long-lived `.env` on disk:
|
||||||
```bash
|
|
||||||
cd /path/to/voxblog
|
|
||||||
cp .env.example .env
|
|
||||||
nano .env
|
|
||||||
```
|
|
||||||
|
|
||||||
Fill in all values from `.env.example`.
|
1. Follow [INFISICAL_SETUP.md](INFISICAL_SETUP.md) to boot Infisical (already live at `https://secrets.pusula.blog`) and add VoxBlog secrets.
|
||||||
|
2. Create a production-scoped service token (scoped to the secret path you created).
|
||||||
|
3. Store the token securely; you'll export it whenever you deploy.
|
||||||
|
|
||||||
## Step 9: Initial Deployment
|
## Step 9: Initial Deployment
|
||||||
|
|
||||||
@ -565,9 +542,12 @@ cd /var/www # or your preferred location
|
|||||||
git clone https://your-gitea-url/your-username/voxblog.git
|
git clone https://your-gitea-url/your-username/voxblog.git
|
||||||
cd voxblog
|
cd voxblog
|
||||||
|
|
||||||
# Create .env file
|
# (Optional) Prepare local .env for development only
|
||||||
cp .env.example .env
|
# cp .env.example .env && nano .env
|
||||||
nano .env # Fill in values
|
|
||||||
|
# Export Infisical token for production deployment
|
||||||
|
export INFISICAL_TOKEN=st.your_service_token
|
||||||
|
export INFISICAL_SITE_URL=https://secrets.pusula.blog
|
||||||
|
|
||||||
# Initial deployment
|
# Initial deployment
|
||||||
./deploy.sh
|
./deploy.sh
|
||||||
@ -650,7 +630,7 @@ git checkout <previous-commit-hash>
|
|||||||
docker system prune -a
|
docker system prune -a
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Use secrets management** - never commit `.env` to git
|
5. **Use secrets management** - keep secrets in Infisical, never commit `.env`
|
||||||
|
|
||||||
6. **Set up monitoring** (optional):
|
6. **Set up monitoring** (optional):
|
||||||
- Portainer for Docker management
|
- Portainer for Docker management
|
||||||
@ -685,7 +665,7 @@ docker system prune -a --volumes
|
|||||||
|
|
||||||
## Security Checklist
|
## Security Checklist
|
||||||
|
|
||||||
- [ ] Use strong passwords in `.env`
|
- [ ] Infisical secrets configured with strong values
|
||||||
- [ ] Enable firewall (ufw)
|
- [ ] Enable firewall (ufw)
|
||||||
- [ ] Keep Docker updated
|
- [ ] Keep Docker updated
|
||||||
- [ ] Use SSL/TLS (HTTPS)
|
- [ ] Use SSL/TLS (HTTPS)
|
||||||
|
|||||||
@ -122,12 +122,15 @@ Gitea detects push
|
|||||||
git clone https://your-gitea-url/username/voxblog.git
|
git clone https://your-gitea-url/username/voxblog.git
|
||||||
cd voxblog
|
cd voxblog
|
||||||
|
|
||||||
# Configure environment
|
# Load Infisical token (preferred)
|
||||||
cp .env.example .env
|
export INFISICAL_TOKEN=st.your_service_token
|
||||||
nano .env # Fill in your values
|
export INFISICAL_SITE_URL=https://secrets.pusula.blog
|
||||||
|
|
||||||
# Deploy!
|
# Deploy!
|
||||||
./deploy.sh
|
./deploy.sh
|
||||||
|
|
||||||
|
# For local testing only
|
||||||
|
# cp .env.example .env && nano .env
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Set Up CI/CD
|
### 2. Set Up CI/CD
|
||||||
@ -150,9 +153,13 @@ sudo apt-get install webhook
|
|||||||
# Configure webhook (see QUICK_START.md)
|
# Configure webhook (see QUICK_START.md)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Add Secrets (Gitea Actions only)
|
### 3. Configure Secrets (Gitea Actions only)
|
||||||
|
|
||||||
Repository → Settings → Secrets → Add all from `.env`
|
Repository → Settings → Secrets:
|
||||||
|
- `INFISICAL_TOKEN` – service token scoped to the production workspace/path.
|
||||||
|
- `INFISICAL_SITE_URL` – `https://secrets.pusula.blog` (already running on your VPS).
|
||||||
|
|
||||||
|
All application variables now live inside Infisical instead of `.env`.
|
||||||
|
|
||||||
### 4. Push to Main
|
### 4. Push to Main
|
||||||
|
|
||||||
@ -164,9 +171,9 @@ git push origin main
|
|||||||
|
|
||||||
🎉 **Auto-deployment triggered!**
|
🎉 **Auto-deployment triggered!**
|
||||||
|
|
||||||
## 🔧 Environment Variables
|
## 🔧 Secrets Inventory
|
||||||
|
|
||||||
All required variables in `.env`:
|
Store these keys inside Infisical (`production` environment):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Database (all use DB_* prefix)
|
# Database (all use DB_* prefix)
|
||||||
@ -255,8 +262,8 @@ docker volume prune
|
|||||||
|
|
||||||
## 🔐 Security Best Practices
|
## 🔐 Security Best Practices
|
||||||
|
|
||||||
- ✅ Use strong passwords in `.env`
|
- ✅ Store strong secrets in Infisical and rotate them regularly
|
||||||
- ✅ Never commit `.env` to git (already in .gitignore)
|
- ✅ Remove stray `.env` files from servers and keep them out of git (already ignored)
|
||||||
- ✅ Enable firewall: `sudo ufw enable`
|
- ✅ Enable firewall: `sudo ufw enable`
|
||||||
- ✅ Use SSL/TLS (HTTPS)
|
- ✅ Use SSL/TLS (HTTPS)
|
||||||
- ✅ Keep Docker updated
|
- ✅ Keep Docker updated
|
||||||
@ -351,10 +358,10 @@ docker-compose exec api env | grep DATABASE
|
|||||||
|
|
||||||
- [ ] Docker files created
|
- [ ] Docker files created
|
||||||
- [ ] docker-compose.yml configured
|
- [ ] docker-compose.yml configured
|
||||||
- [ ] .env file filled with production values
|
- [ ] Infisical production workspace populated with VoxBlog secrets
|
||||||
- [ ] deploy.sh tested locally
|
- [ ] deploy.sh tested locally
|
||||||
- [ ] CI/CD pipeline chosen and configured
|
- [ ] CI/CD pipeline chosen and configured
|
||||||
- [ ] Secrets added to Gitea (if using Actions)
|
- [ ] INFISICAL_TOKEN (+ optional INFISICAL_SITE_URL) added to Gitea secrets
|
||||||
- [ ] Domain DNS configured (optional)
|
- [ ] Domain DNS configured (optional)
|
||||||
- [ ] Nginx reverse proxy set up (optional)
|
- [ ] Nginx reverse proxy set up (optional)
|
||||||
- [ ] SSL certificate installed (optional)
|
- [ ] SSL certificate installed (optional)
|
||||||
|
|||||||
80
INFISICAL_SETUP.md
Normal file
80
INFISICAL_SETUP.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# Infisical Secret Management
|
||||||
|
|
||||||
|
This guide shows how to run (and maintain) the self-hosted Infisical instance for VoxBlog. A production instance is already running on this VPS at `https://secrets.pusula.blog` with files under `/home/adminuser/infisical`.
|
||||||
|
|
||||||
|
## 1. Start Infisical
|
||||||
|
|
||||||
|
1. Copy the sample environment:
|
||||||
|
```bash
|
||||||
|
cp infisical/.env.example infisical/.env
|
||||||
|
```
|
||||||
|
2. Edit `infisical/.env` and set:
|
||||||
|
- `INFISICAL_SITE_URL=https://secrets.pusula.blog`.
|
||||||
|
- `INFISICAL_POSTGRES_PASSWORD` — database password (strong, unique).
|
||||||
|
- `INFISICAL_AUTH_SECRET` — 32-byte base64 secret (`openssl rand -base64 32`).
|
||||||
|
- Leave `INFISICAL_ENCRYPTION_KEY` blank and set `INFISICAL_ROOT_ENCRYPTION_KEY` to a 32-byte base64 secret (`openssl rand -base64 32`).
|
||||||
|
- Set `INFISICAL_DATABASE_URL=postgresql://infisical:${INFISICAL_POSTGRES_PASSWORD}@postgres:5432/infisical`.
|
||||||
|
3. Boot the stack:
|
||||||
|
```bash
|
||||||
|
docker compose \
|
||||||
|
--env-file infisical/.env \
|
||||||
|
-f infisical/docker-compose.yml \
|
||||||
|
up -d
|
||||||
|
```
|
||||||
|
4. Point your reverse proxy (Caddy is already configured) at `http://127.0.0.1:8080` so the public URL works over HTTPS.
|
||||||
|
|
||||||
|
## 2. Bootstrap Infisical
|
||||||
|
|
||||||
|
1. Visit `INFISICAL_SITE_URL` and create the initial admin account.
|
||||||
|
2. Create a **Workspace** (e.g. `voxblog`).
|
||||||
|
3. Add environments you need (at least `production`, maybe `staging`/`development`).
|
||||||
|
4. Inside each environment, create a **secret path** (e.g. `/`) and add the VoxBlog variables:
|
||||||
|
- `MYSQL_ROOT_PASSWORD`
|
||||||
|
- `MYSQL_PASSWORD`
|
||||||
|
- `ADMIN_PASSWORD`
|
||||||
|
- `OPENAI_API_KEY`
|
||||||
|
- `GHOST_ADMIN_API_KEY`
|
||||||
|
- `GHOST_ADMIN_API_URL`
|
||||||
|
- `S3_BUCKET`
|
||||||
|
- `S3_REGION`
|
||||||
|
- `S3_ACCESS_KEY`
|
||||||
|
- `S3_SECRET_KEY`
|
||||||
|
- `S3_ENDPOINT`
|
||||||
|
- `VITE_API_URL`
|
||||||
|
|
||||||
|
## 3. Service Token for Automation
|
||||||
|
|
||||||
|
1. In the workspace, open **Integration → Service Tokens → Create Token**.
|
||||||
|
2. Scope the token to the `production` environment and the secret path containing the keys (usually `/`).
|
||||||
|
3. Copy the token value and store it somewhere safe—you will not see it again.
|
||||||
|
|
||||||
|
## 4. Wire Deployments
|
||||||
|
|
||||||
|
### Gitea Actions
|
||||||
|
|
||||||
|
1. In your VoxBlog repository, go to **Settings → Secrets** and add:
|
||||||
|
- `INFISICAL_TOKEN` — the service token from the previous step.
|
||||||
|
- `INFISICAL_SITE_URL` — `https://secrets.pusula.blog`.
|
||||||
|
2. No other secret variables are needed; the workflow now loads them dynamically before running Docker Compose.
|
||||||
|
|
||||||
|
### Manual / Webhook Deployments
|
||||||
|
|
||||||
|
1. On the VPS, export the token before running `deploy.sh`:
|
||||||
|
```bash
|
||||||
|
export INFISICAL_TOKEN=st.your_token_value
|
||||||
|
export INFISICAL_SITE_URL=https://secrets.pusula.blog
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
2. The script prefers Infisical; it only falls back to `.env` when no token is set, so consider removing any old `.env` file from the server.
|
||||||
|
|
||||||
|
## 5. Rotating Secrets
|
||||||
|
|
||||||
|
1. Update the value inside Infisical.
|
||||||
|
2. Re-run the deployment pipeline (or `deploy.sh`) so new containers launch with the rotated configuration.
|
||||||
|
3. Old values never touch disk—no extra clean-up is required.
|
||||||
|
|
||||||
|
## 6. Backups & Maintenance
|
||||||
|
|
||||||
|
- Backup the Postgres volume (`infisical-postgres-data`) using your usual VPS backup process.
|
||||||
|
- Protect the Infisical site with HTTPS and, ideally, IP allow-lists or SSO.
|
||||||
|
- Rotate the service token periodically; update the Gitea secret and any server-side exports at the same time.
|
||||||
@ -20,29 +20,30 @@ git clone https://your-gitea-url/username/voxblog.git
|
|||||||
cd voxblog
|
cd voxblog
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Configure Environment
|
### Step 2: Configure Secrets
|
||||||
|
|
||||||
|
Recommended (production): export your Infisical service token so the deployment scripts can pull secrets on demand.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Copy example env file
|
# Export the service token (see INFISICAL_SETUP.md)
|
||||||
cp .env.example .env
|
export INFISICAL_TOKEN=st.your_service_token
|
||||||
|
export INFISICAL_SITE_URL=https://secrets.pusula.blog
|
||||||
# Edit with your values
|
export INFISICAL_API_URL=https://secrets.pusula.blog/api # optional, auto-derived otherwise
|
||||||
nano .env
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Fill in all values:
|
Fallback for local-only testing:
|
||||||
- `DB_ROOT_PASSWORD` - Strong password for MySQL root
|
|
||||||
- `DB_PASSWORD` - Password for voxblog database user
|
```bash
|
||||||
- `DB_USER` - Database username (default: voxblog)
|
cp .env.example .env
|
||||||
- `DB_NAME` - Database name (default: voxblog)
|
nano .env # never commit this file
|
||||||
- `DB_HOST` - Database host (default: localhost)
|
```
|
||||||
- `DB_PORT` - Database port (default: 3306)
|
|
||||||
- `ADMIN_PASSWORD` - Password for admin login
|
Whether Infisical or a temporary `.env` is used, ensure these variables exist:
|
||||||
- `OPENAI_API_KEY` - Your OpenAI API key
|
- `DB_ROOT_PASSWORD`, `DB_PASSWORD`, `DB_USER`, `DB_NAME`, `DB_HOST`, `DB_PORT`
|
||||||
- `GHOST_ADMIN_API_KEY` - Your Ghost CMS API key
|
- `ADMIN_PASSWORD`, `OPENAI_API_KEY`
|
||||||
- `GHOST_ADMIN_API_URL` - Ghost Admin API base URL (e.g., https://ghost.example.com)
|
- `GHOST_ADMIN_API_KEY`, `GHOST_ADMIN_API_URL`
|
||||||
- `S3_*` - Your S3 credentials
|
- `S3_BUCKET`, `S3_REGION`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_ENDPOINT`
|
||||||
- `VITE_API_URL` - Your API URL (e.g., https://api.yourdomain.com)
|
- `VITE_API_URL`
|
||||||
|
|
||||||
### Step 3: Deploy
|
### Step 3: Deploy
|
||||||
|
|
||||||
@ -104,22 +105,10 @@ sudo systemctl status gitea-runner
|
|||||||
|
|
||||||
Go to: Repository → Settings → Secrets → Actions
|
Go to: Repository → Settings → Secrets → Actions
|
||||||
|
|
||||||
Add all variables from `.env`:
|
Add:
|
||||||
- `DB_ROOT_PASSWORD`
|
- `INFISICAL_TOKEN` — service token scoped to VoxBlog production.
|
||||||
- `DB_PASSWORD`
|
- `INFISICAL_SITE_URL` — Infisical base URL (optional when self-hosting).
|
||||||
- `DB_USER`
|
- `INFISICAL_API_URL` — Infisical API endpoint (optional; defaults to `${SITE_URL}/api`).
|
||||||
- `DB_NAME`
|
|
||||||
- `DB_HOST`
|
|
||||||
- `DB_PORT`
|
|
||||||
- `ADMIN_PASSWORD`
|
|
||||||
- `OPENAI_API_KEY`
|
|
||||||
- `GHOST_ADMIN_API_KEY`
|
|
||||||
- `S3_BUCKET`
|
|
||||||
- `S3_REGION`
|
|
||||||
- `S3_ACCESS_KEY`
|
|
||||||
- `S3_SECRET_KEY`
|
|
||||||
- `S3_ENDPOINT`
|
|
||||||
- `VITE_API_URL`
|
|
||||||
|
|
||||||
3. **Push to main branch** - Deployment will trigger automatically!
|
3. **Push to main branch** - Deployment will trigger automatically!
|
||||||
|
|
||||||
@ -334,7 +323,7 @@ docker-compose exec mysql mysqldump -u voxblog -p voxblog > db-backup-$(date +%Y
|
|||||||
|
|
||||||
## 🔐 Security Checklist
|
## 🔐 Security Checklist
|
||||||
|
|
||||||
- [ ] Strong passwords in `.env`
|
- [ ] Infisical secrets created with strong values
|
||||||
- [ ] Firewall enabled (ufw)
|
- [ ] Firewall enabled (ufw)
|
||||||
- [ ] SSH key-based authentication
|
- [ ] SSH key-based authentication
|
||||||
- [ ] SSL/TLS enabled (HTTPS)
|
- [ ] SSL/TLS enabled (HTTPS)
|
||||||
@ -344,12 +333,12 @@ docker-compose exec mysql mysqldump -u voxblog -p voxblog > db-backup-$(date +%Y
|
|||||||
|
|
||||||
## 🎯 Production Checklist
|
## 🎯 Production Checklist
|
||||||
|
|
||||||
- [ ] `.env` file configured with production values
|
- [ ] Infisical production workspace populated
|
||||||
- [ ] Domain name pointed to VPS
|
- [ ] Domain name pointed to VPS
|
||||||
- [ ] SSL certificate installed
|
- [ ] SSL certificate installed
|
||||||
- [ ] Nginx reverse proxy configured
|
- [ ] Nginx reverse proxy configured
|
||||||
- [ ] Gitea Actions/Webhook set up
|
- [ ] Gitea Actions/Webhook set up
|
||||||
- [ ] Secrets added to Gitea
|
- [ ] INFISICAL_TOKEN (+ optional INFISICAL_SITE_URL) saved in Gitea secrets
|
||||||
- [ ] Backup strategy in place
|
- [ ] Backup strategy in place
|
||||||
- [ ] Monitoring set up
|
- [ ] Monitoring set up
|
||||||
- [ ] Firewall configured
|
- [ ] Firewall configured
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { defineConfig } from 'drizzle-kit';
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
// Load root .env so CLI has DB creds
|
// Load root .env so CLI has DB creds when available
|
||||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
const envPath = path.resolve(__dirname, '../../.env');
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
dotenv.config({ path: envPath });
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: './src/db/schema.ts',
|
schema: './src/db/schema.ts',
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
|
|
||||||
|
const envPath = path.resolve(__dirname, '../../../.env');
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
dotenv.config({ path: envPath });
|
||||||
|
}
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import morgan from 'morgan';
|
import morgan from 'morgan';
|
||||||
|
|||||||
59
deploy.sh
59
deploy.sh
@ -11,13 +11,64 @@ GREEN='\033[0;32m'
|
|||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
# Check if .env exists
|
fetch_env_from_infisical() {
|
||||||
if [ ! -f .env ]; then
|
if [ -z "${INFISICAL_TOKEN:-}" ]; then
|
||||||
echo -e "${RED}❌ .env file not found!${NC}"
|
echo -e "${RED}❌ INFISICAL_TOKEN is not set${NC}"
|
||||||
echo "Please create .env file from .env.example"
|
echo "Provide an Infisical service token or create a .env file for local use."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}🔐 Retrieving secrets from Infisical...${NC}"
|
||||||
|
local tmp_env
|
||||||
|
tmp_env=$(mktemp)
|
||||||
|
local cli_image
|
||||||
|
cli_image="${INFISICAL_CLI_IMAGE:-infisical/cli:latest}"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2206
|
||||||
|
local docker_cmd=(docker run --rm -e INFISICAL_TOKEN="${INFISICAL_TOKEN}")
|
||||||
|
if [ -n "${INFISICAL_SITE_URL:-}" ]; then
|
||||||
|
docker_cmd+=(-e INFISICAL_SITE_URL="${INFISICAL_SITE_URL}")
|
||||||
|
fi
|
||||||
|
if [ -n "${INFISICAL_API_URL:-}" ]; then
|
||||||
|
docker_cmd+=(-e INFISICAL_API_URL="${INFISICAL_API_URL}")
|
||||||
|
elif [ -n "${INFISICAL_SITE_URL:-}" ]; then
|
||||||
|
local api_url
|
||||||
|
api_url="${INFISICAL_SITE_URL%/}/api"
|
||||||
|
docker_cmd+=(-e INFISICAL_API_URL="${api_url}")
|
||||||
|
fi
|
||||||
|
docker_cmd+=("${cli_image}" export --format=dotenv)
|
||||||
|
|
||||||
|
if ! "${docker_cmd[@]}" >"${tmp_env}"; then
|
||||||
|
echo -e "${RED}❌ Failed to export secrets from Infisical${NC}"
|
||||||
|
rm -f "${tmp_env}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -o allexport
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${tmp_env}"
|
||||||
|
set +o allexport
|
||||||
|
rm -f "${tmp_env}"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_environment() {
|
||||||
|
if [ -n "${INFISICAL_TOKEN:-}" ]; then
|
||||||
|
fetch_env_from_infisical
|
||||||
|
elif [ -f .env ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ Using local .env file (not recommended for production)...${NC}"
|
||||||
|
set -o allexport
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source .env
|
||||||
|
set +o allexport
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ No environment configuration found${NC}"
|
||||||
|
echo "Provide INFISICAL_TOKEN (preferred) or create a local .env for testing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
load_environment
|
||||||
|
|
||||||
# Pull latest code
|
# Pull latest code
|
||||||
echo -e "${YELLOW}📥 Pulling latest code...${NC}"
|
echo -e "${YELLOW}📥 Pulling latest code...${NC}"
|
||||||
git pull origin main
|
git pull origin main
|
||||||
|
|||||||
9
infisical/.env.example
Normal file
9
infisical/.env.example
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Infisical self-hosted configuration
|
||||||
|
INFISICAL_SITE_URL=https://secrets.pusula.blog
|
||||||
|
INFISICAL_POSTGRES_PASSWORD=change-this-password
|
||||||
|
# 32-byte base64 secrets; use `openssl rand -base64 32`
|
||||||
|
INFISICAL_ENCRYPTION_KEY=
|
||||||
|
INFISICAL_ROOT_ENCRYPTION_KEY=replace-with-base64
|
||||||
|
INFISICAL_AUTH_SECRET=replace-with-base64
|
||||||
|
INFISICAL_DATABASE_URL=postgresql://infisical:${INFISICAL_POSTGRES_PASSWORD}@postgres:5432/infisical
|
||||||
|
INFISICAL_TELEMETRY_ENABLED=false
|
||||||
66
infisical/docker-compose.yml
Normal file
66
infisical/docker-compose.yml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: infisical
|
||||||
|
POSTGRES_PASSWORD: ${INFISICAL_POSTGRES_PASSWORD:?set in infisical.env}
|
||||||
|
POSTGRES_DB: infisical
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- infisical
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["redis-server", "--save", "20", "1", "--loglevel", "warning"]
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
networks:
|
||||||
|
- infisical
|
||||||
|
|
||||||
|
infisical:
|
||||||
|
image: infisical/infisical:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: ${INFISICAL_PORT:-8080}
|
||||||
|
HOST: 0.0.0.0
|
||||||
|
SITE_URL: ${INFISICAL_SITE_URL}
|
||||||
|
NEXTAUTH_URL: ${INFISICAL_SITE_URL}
|
||||||
|
NEXT_PUBLIC_SITE_URL: ${INFISICAL_SITE_URL}
|
||||||
|
ENCRYPTION_KEY: ${INFISICAL_ENCRYPTION_KEY:-}
|
||||||
|
ROOT_ENCRYPTION_KEY: ${INFISICAL_ROOT_ENCRYPTION_KEY:?generate_base64_secret}
|
||||||
|
AUTH_SECRET: ${INFISICAL_AUTH_SECRET:?generate_base64_secret}
|
||||||
|
NEXTAUTH_SECRET: ${INFISICAL_AUTH_SECRET:?generate_base64_secret}
|
||||||
|
REDIS_URL: redis://redis:6379
|
||||||
|
DATABASE_URL: ${INFISICAL_DATABASE_URL:?computed_in_env_file}
|
||||||
|
DATABASE_URL_NON_POOLING: ${INFISICAL_DATABASE_URL:?computed_in_env_file}
|
||||||
|
DATABASE_HOST: postgres
|
||||||
|
DATABASE_PORT: 5432
|
||||||
|
DATABASE_USERNAME: infisical
|
||||||
|
DATABASE_PASSWORD: ${INFISICAL_POSTGRES_PASSWORD:?set in infisical.env}
|
||||||
|
DATABASE_NAME: infisical
|
||||||
|
DB_CONNECTION_URI: ${INFISICAL_DATABASE_URL:?computed_in_env_file}
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: infisical
|
||||||
|
DB_PASSWORD: ${INFISICAL_POSTGRES_PASSWORD:?set in infisical.env}
|
||||||
|
DB_NAME: infisical
|
||||||
|
TELEMETRY_ENABLED: ${INFISICAL_TELEMETRY_ENABLED:-false}
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${INFISICAL_PORT:-8080}:8080"
|
||||||
|
networks:
|
||||||
|
- infisical
|
||||||
|
|
||||||
|
networks:
|
||||||
|
infisical:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
redis-data:
|
||||||
Loading…
Reference in New Issue
Block a user