Not responsible for melted desks, burnt thighs, or AppleCare claims.
This guide sets up an improved local development environment with:
- just as a task runner (replacement for make)
- portless for
.testdomain names instead oflocalhost:port - Git worktrees for working on multiple tasks in parallel, each with its own isolated Docker stack
- mitmproxy for inspecting HTTP requests between frontend and backend
- Mailpit as a standalone SMTP server shared across all stacks
- tsgo (native TypeScript compiler) for 10x faster type checking
brew install just
npm install -g portlessmkdir -p ~/.zsh/completions
just --completions zsh > ~/.zsh/completions/_justAdd to ~/.zshrc:
fpath=(~/.zsh/completions $fpath)
autoload -U compinit
compinitportless proxy start --tld test- Disk image size: 120 GB (Settings → Resources)
- Memory: 16 GB (Settings → Resources)
All files are local to your clone — nothing gets committed. They are ignored via .git/info/exclude.
Append these lines to your existing .git/info/exclude:
# Personal dev setup
docker-compose.dev.override.yml
docker-compose.worktree.yml
docker-compose.worktree.debug.yml
justfile
README.dev.md
nginx/default.dev.debug.conf
# Default project name is the current directory name
default_name := `basename "$PWD"`
# --- Main dev environment (existing workflow) ---
up-dev:
#!/usr/bin/env bash
set -euo pipefail
# Update .env with the correct site URL
grep -q '^SITE_URL=' .env && sed -i '' 's|^SITE_URL=.*|SITE_URL=https://infisical.test|' .env || echo 'SITE_URL=https://infisical.test' >> .env
grep -q '^VITE_ALLOWED_HOSTS=' .env && sed -i '' 's|^VITE_ALLOWED_HOSTS=.*|VITE_ALLOWED_HOSTS=infisical.test|' .env || echo 'VITE_ALLOWED_HOSTS=infisical.test' >> .env
docker compose -f docker-compose.dev.yml -f docker-compose.dev.override.yml up --build -d
portless alias infisical 8080 --tld test --force 2>/dev/null || true
portless alias db.infisical 5432 --tld test --force 2>/dev/null || true
portless alias redis.infisical 6379 --tld test --force 2>/dev/null || true
@echo ""
@echo "App: https://infisical.test"
@echo "Postgres: db.infisical.test (port 5432)"
@echo "Redis: redis.infisical.test (port 6379)"
@echo "psql: psql -h db.infisical.test -U infisical -d infisical"
up-dev-ldap:
docker compose -f docker-compose.dev.yml -f docker-compose.dev.override.yml --profile ldap up --build -d
portless alias infisical 8080 --tld test --force 2>/dev/null || true
portless alias db.infisical 5432 --tld test --force 2>/dev/null || true
portless alias redis.infisical 6379 --tld test --force 2>/dev/null || true
@echo ""
@echo "App: https://infisical.test"
@echo "Postgres: db.infisical.test (port 5432)"
@echo "Redis: redis.infisical.test (port 6379)"
up-dev-metrics:
docker compose -f docker-compose.dev.yml -f docker-compose.dev.override.yml --profile metrics up --build -d
portless alias infisical 8080 --tld test --force 2>/dev/null || true
portless alias db.infisical 5432 --tld test --force 2>/dev/null || true
portless alias redis.infisical 6379 --tld test --force 2>/dev/null || true
@echo ""
@echo "App: https://infisical.test"
@echo "Postgres: db.infisical.test (port 5432)"
@echo "Redis: redis.infisical.test (port 6379)"
up-dev-sso:
docker compose -f docker-compose.dev.yml -f docker-compose.dev.override.yml --profile sso up --build -d
portless alias infisical 8080 --tld test --force 2>/dev/null || true
portless alias db.infisical 5432 --tld test --force 2>/dev/null || true
portless alias redis.infisical 6379 --tld test --force 2>/dev/null || true
@echo ""
@echo "App: https://infisical.test"
@echo "Postgres: db.infisical.test (port 5432)"
@echo "Redis: redis.infisical.test (port 6379)"
up-prod:
docker compose -f docker-compose.prod.yml up --build
down-dev:
docker compose -f docker-compose.dev.yml down
reviewable-ui:
cd frontend && npm run lint:fix && npm run type:check
reviewable-api:
cd backend && npm run lint:fix && npm run type:check
reviewable: reviewable-ui reviewable-api
# --- Worktree stacks ---
# Start a full isolated stack for the current worktree
# Usage: just up [name] [--no-fips]
up name=default_name *flags="":
#!/usr/bin/env bash
set -euo pipefail
NAME="{{name}}"
PG_SOURCE="infisical_postgres-data1"
# Check for --no-fips flag
for flag in {{flags}}; do
if [ "$flag" = "--no-fips" ]; then
PG_SOURCE="infisical_postgres-data"
fi
done
PG_VOLUME="${NAME}_postgres-data"
# Clone the postgres volume if this task doesn't have one yet
if ! docker volume inspect "$PG_VOLUME" > /dev/null 2>&1; then
echo "Cloning $PG_SOURCE → $PG_VOLUME..."
docker volume create "$PG_VOLUME" > /dev/null
docker run --rm \
-v "$PG_SOURCE":/from:ro \
-v "$PG_VOLUME":/to \
alpine sh -c "cp -a /from/. /to/"
echo "Done."
else
echo "Volume $PG_VOLUME already exists, reusing."
fi
# Update .env with the correct site URL
if [ -f .env ]; then
grep -q '^SITE_URL=' .env && sed -i '' "s|^SITE_URL=.*|SITE_URL=https://$NAME.test|" .env || echo "SITE_URL=https://$NAME.test" >> .env
grep -q '^VITE_ALLOWED_HOSTS=' .env && sed -i '' "s|^VITE_ALLOWED_HOSTS=.*|VITE_ALLOWED_HOSTS=$NAME.test|" .env || echo "VITE_ALLOWED_HOSTS=$NAME.test" >> .env
fi
# Start the stack
PG_VOLUME="$PG_VOLUME" docker compose \
-p "$NAME" \
-f docker-compose.worktree.yml \
up --build -d
# Wait a moment for containers to get ports assigned
sleep 2
# Get the nginx container's host port
NGINX_CONTAINER="${NAME}-nginx-1"
NGINX_PORT=$(docker port "$NGINX_CONTAINER" 80 2>/dev/null | head -1 | cut -d: -f2)
# Get the postgres container's host port
DB_CONTAINER="${NAME}-db-1"
DB_PORT=$(docker port "$DB_CONTAINER" 5432 2>/dev/null | head -1 | cut -d: -f2)
# Get the redis container's host port
REDIS_CONTAINER="${NAME}-redis-1"
REDIS_PORT=$(docker port "$REDIS_CONTAINER" 6379 2>/dev/null | head -1 | cut -d: -f2)
# Register with portless
portless alias "$NAME" "$NGINX_PORT" --tld test --force 2>/dev/null || true
portless alias "db.$NAME" "$DB_PORT" --tld test --force 2>/dev/null || true
portless alias "redis.$NAME" "$REDIS_PORT" --tld test --force 2>/dev/null || true
echo ""
echo "Stack '$NAME' is running:"
echo " App: https://$NAME.test"
echo " Postgres: db.$NAME.test (port $DB_PORT)"
echo " Redis: redis.$NAME.test (port $REDIS_PORT)"
echo " psql: psql -h db.$NAME.test -U infisical -d infisical"
# Stop a worktree stack (keeps volumes)
down name=default_name:
#!/usr/bin/env bash
set -euo pipefail
NAME="{{name}}"
docker compose \
-p "$NAME" \
-f docker-compose.worktree.yml \
-f docker-compose.worktree.debug.yml \
down 2>/dev/null || \
docker compose \
-p "$NAME" \
-f docker-compose.worktree.yml \
down
# Remove portless aliases
portless alias --remove "$NAME" 2>/dev/null || true
portless alias --remove "debug.$NAME" 2>/dev/null || true
portless alias --remove "db.$NAME" 2>/dev/null || true
portless alias --remove "redis.$NAME" 2>/dev/null || true
echo "Stack '$NAME' stopped. Volumes preserved."
# Remove a worktree stack's containers AND volumes
rm name=default_name:
#!/usr/bin/env bash
set -euo pipefail
NAME="{{name}}"
docker compose \
-p "$NAME" \
-f docker-compose.worktree.yml \
-f docker-compose.worktree.debug.yml \
down -v 2>/dev/null || \
docker compose \
-p "$NAME" \
-f docker-compose.worktree.yml \
down -v
# Remove the cloned postgres volume
docker volume rm "${NAME}_postgres-data" 2>/dev/null || true
# Remove portless aliases
portless alias --remove "$NAME" 2>/dev/null || true
portless alias --remove "debug.$NAME" 2>/dev/null || true
portless alias --remove "db.$NAME" 2>/dev/null || true
portless alias --remove "redis.$NAME" 2>/dev/null || true
echo "Stack '$NAME' removed (containers + volumes)."
# Start a worktree stack with mitmproxy (request inspector)
up-debug name=default_name *flags="":
#!/usr/bin/env bash
set -euo pipefail
NAME="{{name}}"
PG_SOURCE="infisical_postgres-data1"
# Check for --no-fips flag
for flag in {{flags}}; do
if [ "$flag" = "--no-fips" ]; then
PG_SOURCE="infisical_postgres-data"
fi
done
PG_VOLUME="${NAME}_postgres-data"
# Clone the postgres volume if this task doesn't have one yet
if ! docker volume inspect "$PG_VOLUME" > /dev/null 2>&1; then
echo "Cloning $PG_SOURCE → $PG_VOLUME..."
docker volume create "$PG_VOLUME" > /dev/null
docker run --rm \
-v "$PG_SOURCE":/from:ro \
-v "$PG_VOLUME":/to \
alpine sh -c "cp -a /from/. /to/"
echo "Done."
else
echo "Volume $PG_VOLUME already exists, reusing."
fi
# Update .env with the correct site URL
if [ -f .env ]; then
grep -q '^SITE_URL=' .env && sed -i '' "s|^SITE_URL=.*|SITE_URL=https://$NAME.test|" .env || echo "SITE_URL=https://$NAME.test" >> .env
grep -q '^VITE_ALLOWED_HOSTS=' .env && sed -i '' "s|^VITE_ALLOWED_HOSTS=.*|VITE_ALLOWED_HOSTS=$NAME.test|" .env || echo "VITE_ALLOWED_HOSTS=$NAME.test" >> .env
fi
# Start the stack with mitmproxy
PG_VOLUME="$PG_VOLUME" docker compose \
-p "$NAME" \
-f docker-compose.worktree.yml \
-f docker-compose.worktree.debug.yml \
up --build -d
# Wait a moment for containers to get ports assigned
sleep 2
# Get the nginx container's host port
NGINX_CONTAINER="${NAME}-nginx-1"
NGINX_PORT=$(docker port "$NGINX_CONTAINER" 80 2>/dev/null | head -1 | cut -d: -f2)
# Get the mitmweb UI port
MITM_CONTAINER="${NAME}-mitmproxy-1"
MITM_PORT=$(docker port "$MITM_CONTAINER" 8081 2>/dev/null | head -1 | cut -d: -f2)
# Get the postgres container's host port
DB_CONTAINER="${NAME}-db-1"
DB_PORT=$(docker port "$DB_CONTAINER" 5432 2>/dev/null | head -1 | cut -d: -f2)
# Get the redis container's host port
REDIS_CONTAINER="${NAME}-redis-1"
REDIS_PORT=$(docker port "$REDIS_CONTAINER" 6379 2>/dev/null | head -1 | cut -d: -f2)
# Register with portless
portless alias "$NAME" "$NGINX_PORT" --tld test --force 2>/dev/null || true
portless alias "debug.$NAME" "$MITM_PORT" --tld test --force 2>/dev/null || true
portless alias "db.$NAME" "$DB_PORT" --tld test --force 2>/dev/null || true
portless alias "redis.$NAME" "$REDIS_PORT" --tld test --force 2>/dev/null || true
echo ""
echo "Stack '$NAME' is running with mitmproxy:"
echo " App: https://$NAME.test"
echo " mitmweb: https://debug.$NAME.test (request inspector, password: password)"
echo " Postgres: db.$NAME.test (port $DB_PORT)"
echo " Redis: redis.$NAME.test (port $REDIS_PORT)"
echo " psql: psql -h db.$NAME.test -U infisical -d infisical"
# List all running worktree stacks
ls:
@docker ps --filter "label=com.docker.compose.project" --format "table {{{{.Names}}}}\t{{{{.Status}}}}\t{{{{.Ports}}}}" | head -50
# --- Standalone tools ---
mailpit:
docker run -d --name mailpit -p 1025:1025 -p 8025:8025 --restart unless-stopped axllent/mailpit
mailpit-stop:
docker stop mailpit && docker rm mailpit
# --- TypeScript native compiler (tsgo) ---
tsgo-fe:
cd frontend && npm install --save-dev @typescript/native-preview@latest
tsgo-be:
cd backend && npm install --save-dev @typescript/native-preview@latest
tsgo: tsgo-fe tsgo-beProfiles out pgAdmin, redis-commander, and smtp-server from the default dev stack so they only start with --profile tools.
services:
pgadmin:
profiles: [tools]
redis-commander:
profiles: [tools]
smtp-server:
profiles: [tools]Full isolated stack for worktrees. All host ports are random (0:port) to avoid conflicts when running multiple stacks.
services:
nginx:
image: nginx
restart: "always"
ports:
- "0:80"
volumes:
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- backend
- frontend
db:
image: postgres:14-alpine
ports:
- "0:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: infisical
POSTGRES_USER: infisical
POSTGRES_DB: infisical
redis:
image: redis
environment:
- ALLOW_EMPTY_PASSWORD=yes
ports:
- "0:6379"
volumes:
- redis_data:/data
clickhouse:
image: clickhouse/clickhouse-server:25.12.5
restart: unless-stopped
ports:
- "0:8123"
- "0:9000"
volumes:
- clickhouse_data:/var/lib/clickhouse
- clickhouse_logs:/var/log/clickhouse-server
environment:
- CLICKHOUSE_DB=infisical
- CLICKHOUSE_USER=infisical
- CLICKHOUSE_PASSWORD=infisical
- CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1
ulimits:
nofile:
soft: 262144
hard: 262144
backend:
image: infisical-dev-backend:latest
build:
context: ./backend
dockerfile: Dockerfile.dev.fips
depends_on:
db:
condition: service_started
redis:
condition: service_started
clickhouse:
condition: service_started
env_file:
- .env
ports:
- "0:4000"
environment:
- NODE_ENV=development
- DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable
- TELEMETRY_ENABLED=false
volumes:
- ./backend/src:/app/src
- softhsm_tokens:/etc/softhsm2/tokens
extra_hosts:
- "host.docker.internal:host-gateway"
frontend:
image: infisical-dev-frontend:latest
restart: unless-stopped
depends_on:
- backend
build:
context: ./frontend
dockerfile: Dockerfile.dev
volumes:
- ./frontend/src:/app/src/
- ./frontend/public:/app/public
env_file: .env
volumes:
postgres_data:
external: true
name: ${PG_VOLUME}
redis_data:
driver: local
clickhouse_data:
driver: local
clickhouse_logs:
driver: local
softhsm_tokens:
driver: localCompose override that adds mitmproxy between nginx and backend for request inspection.
services:
mitmproxy:
image: mitmproxy/mitmproxy
command: mitmweb --mode reverse:http://backend:4000 --web-host 0.0.0.0 --listen-port 4000 --no-web-open-browser --set web_password=password
restart: unless-stopped
ports:
- "0:8081"
depends_on:
- backend
nginx:
volumes:
- ./nginx/default.dev.debug.conf:/etc/nginx/conf.d/default.conf:roSame as nginx/default.dev.conf but routes API traffic through mitmproxy instead of directly to backend. Only difference is line 2:
upstream api {
server mitmproxy:4000;
}
server {
listen 80;
large_client_header_buffers 8 128k;
client_header_buffer_size 128k;
# WebSocket endpoint for PAM web access
location ~ ^/api/v1/pam/accounts/[^/]+/web-access$ {
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 4200s;
proxy_send_timeout 4200s;
proxy_pass http://api;
proxy_redirect off;
proxy_cookie_path / "/; SameSite=strict";
}
location ~ ^/(api|secret-scanning/webhooks) {
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://api;
proxy_redirect off;
proxy_cookie_path / "/; SameSite=strict";
}
location /runtime-ui-env.js {
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://api;
proxy_redirect off;
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location /api/v3/migrate {
client_max_body_size 25M;
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://api;
proxy_redirect off;
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location /scep {
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://api;
proxy_redirect off;
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location ~ /\.well-known {
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;
proxy_pass http://api;
proxy_redirect off;
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location / {
include /etc/nginx/mime.types;
proxy_set_header X-Real-RIP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://frontend:3000;
proxy_redirect off;
}
}Git hook that automatically copies all dev files into new worktrees. Must be executable (chmod +x).
#!/bin/bash
OLD_REF=$1
# Only run for new worktrees (old ref is all zeros)
if [ "$OLD_REF" = "0000000000000000000000000000000000000000" ]; then
MAIN_REPO="$(git rev-parse --git-common-dir)/.."
WORKTREE="$(git rev-parse --show-toplevel)"
cp "$MAIN_REPO/justfile" "$WORKTREE/" 2>/dev/null
cp "$MAIN_REPO/docker-compose.worktree.yml" "$WORKTREE/" 2>/dev/null
cp "$MAIN_REPO/docker-compose.worktree.debug.yml" "$WORKTREE/" 2>/dev/null
cp "$MAIN_REPO/docker-compose.dev.override.yml" "$WORKTREE/" 2>/dev/null
cp "$MAIN_REPO/README.dev.md" "$WORKTREE/" 2>/dev/null
cp "$MAIN_REPO/.env" "$WORKTREE/" 2>/dev/null
mkdir -p "$WORKTREE/nginx"
cp "$MAIN_REPO/nginx/default.dev.debug.conf" "$WORKTREE/nginx/" 2>/dev/null
echo "Dev files copied into worktree: $WORKTREE"
fiHusky overrides the default hooks directory. To use our custom hook alongside Husky's pre-commit:
# Create local hooks directory
mkdir -p .git/local-hooks/_
# Copy Husky's pre-commit hook and helper
cp .husky/pre-commit .git/local-hooks/
cp .husky/_/husky.sh .git/local-hooks/_/
# Copy the post-checkout hook (from step 7 above)
# Make it executable
chmod +x .git/local-hooks/post-checkout
chmod +x .git/local-hooks/pre-commit
# Point git to the local hooks directory
git config --local core.hooksPath .git/local-hooksjust up-dev # → https://infisical.test
just down-dev # stop# Create worktree (hook auto-copies dev files + .env)
git fetch origin
git worktree add ../my-feature -b feat/my-feature origin/main
cd ../my-feature
# Optional: enable fast TS compiler
just tsgo
# Start isolated stack
just up # → https://my-feature.testgit fetch origin
git worktree add ../review-pr origin/feat/some-feature
cd ../review-pr
just up # → https://review-pr.testBoth run at the same time on different domains:
https://my-feature.testhttps://review-pr.test
just up-debug # → app at https://my-feature.test
# → mitmweb UI at https://debug.my-feature.test (password: password)just down # stop containers, keep volumes (resume later)
just up # resume with same DB state
just rm # remove containers + volumes (permanent)
# Remove the worktree
cd ~/infi/repos/infisical
git worktree remove ../review-prjust mailpit # start — SMTP on 1025, UI on http://localhost:8025
just mailpit-stop # stop| Command | What it does |
|---|---|
just up-dev |
Start main dev stack → https://infisical.test |
just up-dev-ldap |
Start with LDAP profile |
just up-dev-sso |
Start with SSO profile |
just up-dev-metrics |
Start with metrics profile |
just down-dev |
Stop main dev stack |
just up |
Start worktree stack (uses directory name) |
just up my-name |
Start with custom name |
just up --no-fips |
Clone non-FIPS postgres volume |
just up-debug |
Start with mitmproxy request inspector |
just down |
Stop worktree stack, keep volumes |
just rm |
Remove worktree stack + volumes |
just ls |
List all running stacks |
just mailpit |
Start standalone mailpit |
just mailpit-stop |
Stop mailpit |
just tsgo |
Install latest tsgo in both frontend and backend |
just tsgo-fe |
Install latest tsgo in frontend only |
just tsgo-be |
Install latest tsgo in backend only |
just reviewable |
Lint + typecheck both frontend and backend |
just reviewable-ui |
Lint + typecheck frontend |
just reviewable-api |
Lint + typecheck backend |