Skip to content

Commit 67fc0ca

Browse files
NotYuShengclaude
andauthored
feat: add offline deployment support (#198)
* fix: address review comments on sub-issue template - Add 'sub-issue' default label for consistency with other templates - Remove unnecessary backslash escaping from placeholder brackets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add offline deployment support with image save/load scripts Adds docker-compose.offline.yml and two helper scripts for deploying TracePcap in air-gapped environments: pull-and-save-images.sh (builds and exports all images as tars) and load-images.sh (loads them on the offline host). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix/offline-deploy-review-comments - Replace curl with wget --spider in minio healthcheck (more portable on minimal images) - Default LLM_API_BASE_URL to Ollama local endpoint instead of OpenAI public API - Pin minio/minio and minio/mc to specific release tags (non-deterministic latest removed) - Allow VITE_API_BASE_URL to be overridden via .env using ${VAR:-default} pattern - Loop over DOCKERHUB_IMAGES array to save tars instead of hardcoding image names Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add offline deployment instructions to README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: use LM Studio as default LLM example for offline deployment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 262e90c commit 67fc0ca

5 files changed

Lines changed: 337 additions & 4 deletions

File tree

.github/ISSUE_TEMPLATE/sub-issue.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22
name: Sub-Issue Template
33
about: Use this for tracking sub-tasks under major issues
44
title: "[Sub-Issue] "
5-
labels:
5+
labels: sub-issue
66
assignees: ''
77
---
88

9-
### Sub-Issue: \[Brief Description of the Sub-Task]
9+
### Sub-Issue: [Brief Description of the Sub-Task]
1010

1111
**Related to**: #X
1212

1313
---
1414

1515
### What needs to be done
1616

17-
\[Clearly describe the scope and purpose of this sub-issue. If applicable, mention relevant existing code, its limitations, and what the outcome of this sub-issue should achieve.]
17+
[Clearly describe the scope and purpose of this sub-issue. If applicable, mention relevant existing code, its limitations, and what the outcome of this sub-issue should achieve.]
1818

1919
---
2020

@@ -33,4 +33,4 @@ assignees: ''
3333

3434
### Why this is needed
3535

36-
\[Explain why the change improves maintainability, reusability, or correctness. Focus on long-term benefits like reducing duplication, simplifying onboarding, or decoupling service responsibilities.]
36+
[Explain why the change improves maintainability, reusability, or correctness. Focus on long-term benefits like reducing duplication, simplifying onboarding, or decoupling service responsibilities.]

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,42 @@ Navigate to **http://localhost:80/swagger-ui.html** to explore the API interacti
199199

200200
TracePcap is designed for self-hosted deployment:
201201

202+
### Offline / Air-gapped Deployment
203+
204+
For environments without internet access, use the offline deployment workflow:
205+
206+
**On an internet-connected machine:**
207+
208+
```bash
209+
# Pull all third-party images, build local images, and save everything as .tar files
210+
bash scripts/pull-and-save-images.sh
211+
```
212+
213+
This creates a `images/` directory containing `.tar` files for all services.
214+
215+
**Transfer to the offline machine:**
216+
217+
```
218+
images/ # all .tar files
219+
docker-compose.offline.yml
220+
scripts/load-images.sh
221+
.env # copy from .env.example and configure
222+
```
223+
224+
**On the offline machine:**
225+
226+
```bash
227+
# Load all images into Docker
228+
bash scripts/load-images.sh
229+
230+
# Start the stack
231+
docker compose -f docker-compose.offline.yml up -d
232+
```
233+
234+
> **Note**: The offline compose file defaults `LLM_API_BASE_URL` to `http://localhost:1234/v1` (LM Studio). Configure a locally-hosted LLM in `.env` before starting if you want AI features.
235+
236+
---
237+
202238
- **Development**: Use built-in configuration with exposed ports
203239
- **Production**:
204240
- Change default MinIO credentials in `docker-compose.yml`

docker-compose.offline.yml

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# docker-compose.offline.yml
2+
#
3+
# Offline deployment variant — uses pre-built images loaded via scripts/load-images.sh
4+
# instead of building from source.
5+
#
6+
# Prerequisites:
7+
# 1. On an internet-connected machine: bash scripts/pull-and-save-images.sh
8+
# 2. Transfer images/, this file, and .env to the offline machine.
9+
# 3. On the offline machine: bash scripts/load-images.sh
10+
#
11+
# Start: docker compose -f docker-compose.offline.yml up -d
12+
# Stop: docker compose -f docker-compose.offline.yml down
13+
14+
services:
15+
# PostgreSQL Database
16+
postgres:
17+
image: postgres:15-alpine
18+
container_name: tracepcap-postgres
19+
environment:
20+
POSTGRES_DB: tracepcap
21+
POSTGRES_USER: tracepcap_user
22+
POSTGRES_PASSWORD: tracepcap_pass
23+
TZ: Asia/Singapore
24+
ports:
25+
- "5432:5432"
26+
volumes:
27+
- postgres_data:/var/lib/postgresql/data
28+
healthcheck:
29+
test: ["CMD-SHELL", "pg_isready -U tracepcap_user -d tracepcap"]
30+
interval: 10s
31+
timeout: 5s
32+
retries: 5
33+
networks:
34+
- tracepcap-network
35+
36+
# MinIO Object Storage
37+
minio:
38+
image: minio/minio:RELEASE.2024-11-07T00-52-20Z
39+
container_name: tracepcap-minio
40+
command: server /data --console-address ":9001"
41+
environment:
42+
MINIO_ROOT_USER: minioadmin
43+
MINIO_ROOT_PASSWORD: minioadmin
44+
TZ: Asia/Singapore
45+
ports:
46+
- "9000:9000" # API
47+
- "9001:9001" # Console
48+
volumes:
49+
- minio_data:/data
50+
healthcheck:
51+
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9000/minio/health/live"]
52+
interval: 30s
53+
timeout: 20s
54+
retries: 3
55+
networks:
56+
- tracepcap-network
57+
58+
# MinIO Client (mc) - Create bucket on startup
59+
minio-init:
60+
image: minio/mc:RELEASE.2024-11-21T17-21-54Z
61+
container_name: tracepcap-minio-init
62+
depends_on:
63+
- minio
64+
entrypoint: >
65+
/bin/sh -c "
66+
sleep 5;
67+
/usr/bin/mc alias set myminio http://minio:9000 minioadmin minioadmin;
68+
/usr/bin/mc mb myminio/tracepcap-files --ignore-existing;
69+
/usr/bin/mc anonymous set public myminio/tracepcap-files;
70+
exit 0;
71+
"
72+
networks:
73+
- tracepcap-network
74+
75+
# Spring Boot Backend (pre-built image)
76+
backend:
77+
image: tracepcap-backend:latest
78+
container_name: tracepcap-backend
79+
environment:
80+
SPRING_PROFILES_ACTIVE: dev
81+
DATABASE_URL: jdbc:postgresql://postgres:5432/tracepcap
82+
DATABASE_USERNAME: tracepcap_user
83+
DATABASE_PASSWORD: tracepcap_pass
84+
MINIO_ENDPOINT: http://minio:9000
85+
MINIO_ACCESS_KEY: minioadmin
86+
MINIO_SECRET_KEY: minioadmin
87+
MINIO_BUCKET: tracepcap-files
88+
APP_MEMORY_MB: ${APP_MEMORY_MB:-2048}
89+
LLM_API_BASE_URL: ${LLM_API_BASE_URL:-http://localhost:1234/v1}
90+
LLM_API_KEY: ${LLM_API_KEY:-your-api-key-here}
91+
LLM_MODEL: ${LLM_MODEL:-gpt-4}
92+
LLM_TEMPERATURE: ${LLM_TEMPERATURE:-0.7}
93+
LLM_MAX_TOKENS: ${LLM_MAX_TOKENS:-2000}
94+
LLM_TIMEOUT: ${LLM_TIMEOUT:-300}
95+
TZ: Asia/Singapore
96+
volumes:
97+
- config_data:/app/config
98+
depends_on:
99+
postgres:
100+
condition: service_healthy
101+
minio:
102+
condition: service_healthy
103+
networks:
104+
- tracepcap-network
105+
106+
# Nginx - Serves frontend and proxies API to backend (pre-built image)
107+
# Note: frontend build args (VITE_*) are baked into this image at build time.
108+
# To change them, rebuild the image with pull-and-save-images.sh.
109+
nginx:
110+
image: tracepcap-nginx:latest
111+
container_name: tracepcap-nginx
112+
environment:
113+
APP_MEMORY_MB: ${APP_MEMORY_MB:-2048}
114+
TZ: Asia/Singapore
115+
ports:
116+
- "${NGINX_PORT:-80}:80"
117+
depends_on:
118+
- backend
119+
networks:
120+
- tracepcap-network
121+
122+
volumes:
123+
postgres_data:
124+
driver: local
125+
minio_data:
126+
driver: local
127+
config_data:
128+
driver: local
129+
130+
networks:
131+
tracepcap-network:
132+
driver: bridge

scripts/load-images.sh

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env bash
2+
# load-images.sh
3+
#
4+
# Run this on the OFFLINE machine after copying the images/ folder here.
5+
#
6+
# What it does:
7+
# Loads every .tar file in ./images/ into the local Docker daemon.
8+
#
9+
# Usage:
10+
# bash scripts/load-images.sh
11+
#
12+
# After loading, start the stack with:
13+
# docker compose -f docker-compose.offline.yml up -d
14+
15+
set -euo pipefail
16+
17+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18+
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
19+
IMAGES_DIR="$ROOT_DIR/images"
20+
21+
if [ ! -d "$IMAGES_DIR" ]; then
22+
echo "Error: images/ directory not found at $IMAGES_DIR"
23+
echo "Make sure you copied the images/ folder from the internet-connected machine."
24+
exit 1
25+
fi
26+
27+
# Collect .tar files
28+
shopt -s nullglob
29+
TAR_FILES=("$IMAGES_DIR"/*.tar)
30+
shopt -u nullglob
31+
32+
if [ ${#TAR_FILES[@]} -eq 0 ]; then
33+
echo "Error: No .tar files found in $IMAGES_DIR/"
34+
echo "Run pull-and-save-images.sh on an internet-connected machine first."
35+
exit 1
36+
fi
37+
38+
echo "=== Loading Docker images from images/ ==="
39+
echo ""
40+
for tarfile in "${TAR_FILES[@]}"; do
41+
echo " Loading $(basename "$tarfile")..."
42+
docker load -i "$tarfile"
43+
done
44+
45+
echo ""
46+
echo "=== All images loaded successfully ==="
47+
echo ""
48+
echo "Start the application with:"
49+
echo " docker compose -f docker-compose.offline.yml up -d"

scripts/pull-and-save-images.sh

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env bash
2+
# pull-and-save-images.sh
3+
#
4+
# Run this on an internet-connected machine BEFORE transferring to the offline host.
5+
#
6+
# What it does:
7+
# 1. Pulls all third-party images from Docker Hub
8+
# 2. Builds the backend and nginx images locally
9+
# 3. Saves every image as a .tar file under ./images/
10+
#
11+
# Usage:
12+
# bash scripts/pull-and-save-images.sh
13+
#
14+
# Build args for nginx are read from .env (if present) — copy .env.example first
15+
# if you haven't already configured it.
16+
17+
set -euo pipefail
18+
19+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20+
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
21+
IMAGES_DIR="$ROOT_DIR/images"
22+
23+
BACKEND_IMAGE="tracepcap-backend:latest"
24+
NGINX_IMAGE="tracepcap-nginx:latest"
25+
26+
# ---------------------------------------------------------------------------
27+
# Helper
28+
# ---------------------------------------------------------------------------
29+
save_image() {
30+
local image="$1"
31+
local filename="$2"
32+
echo " Saving $image -> images/$filename"
33+
docker save "$image" -o "$IMAGES_DIR/$filename"
34+
}
35+
36+
# ---------------------------------------------------------------------------
37+
# Load build-arg overrides from .env when available
38+
# ---------------------------------------------------------------------------
39+
if [ -f "$ROOT_DIR/.env" ]; then
40+
echo "Loading build args from .env"
41+
set -a
42+
# shellcheck source=/dev/null
43+
source "$ROOT_DIR/.env"
44+
set +a
45+
fi
46+
47+
mkdir -p "$IMAGES_DIR"
48+
49+
# ---------------------------------------------------------------------------
50+
# 1. Pull third-party images
51+
# ---------------------------------------------------------------------------
52+
echo ""
53+
echo "=== [1/3] Pulling third-party images ==="
54+
55+
# --- Docker Hub ---
56+
DOCKERHUB_IMAGES=(
57+
"postgres:15-alpine"
58+
"minio/minio:RELEASE.2024-11-07T00-52-20Z"
59+
"minio/mc:RELEASE.2024-11-21T17-21-54Z"
60+
)
61+
62+
for img in "${DOCKERHUB_IMAGES[@]}"; do
63+
echo " Pulling $img (Docker Hub)..."
64+
docker pull "$img"
65+
done
66+
67+
# ---------------------------------------------------------------------------
68+
# 2. Build local images
69+
# ---------------------------------------------------------------------------
70+
echo ""
71+
echo "=== [2/3] Building local images ==="
72+
cd "$ROOT_DIR"
73+
74+
echo " Building backend..."
75+
docker build \
76+
-t "$BACKEND_IMAGE" \
77+
./backend
78+
79+
echo " Building nginx (frontend)..."
80+
docker build \
81+
--build-arg "VITE_API_BASE_URL=${VITE_API_BASE_URL:-/api}" \
82+
--build-arg "VITE_SUPPORTED_FILE_TYPES=${VITE_SUPPORTED_FILE_TYPES:-.pcap,.pcapng,.cap}" \
83+
--build-arg "VITE_ANALYSIS_OPTIONS=${VITE_ANALYSIS_OPTIONS:-false}" \
84+
--build-arg "VITE_NETWORK_DIAGRAM_CONVERSATION_LIMIT=${VITE_NETWORK_DIAGRAM_CONVERSATION_LIMIT:-false}" \
85+
-t "$NGINX_IMAGE" \
86+
-f ./nginx/Dockerfile \
87+
.
88+
89+
# ---------------------------------------------------------------------------
90+
# 3. Save all images as tars
91+
# ---------------------------------------------------------------------------
92+
echo ""
93+
echo "=== [3/3] Saving images to images/ ==="
94+
95+
for img in "${DOCKERHUB_IMAGES[@]}"; do
96+
filename="$(echo "$img" | tr '/:' '_').tar"
97+
save_image "$img" "$filename"
98+
done
99+
save_image "$BACKEND_IMAGE" "tracepcap-backend.tar"
100+
save_image "$NGINX_IMAGE" "tracepcap-nginx.tar"
101+
102+
# ---------------------------------------------------------------------------
103+
# Summary
104+
# ---------------------------------------------------------------------------
105+
echo ""
106+
echo "=== Done ==="
107+
echo ""
108+
echo "Transfer the following to the offline machine:"
109+
echo " images/ (all .tar files)"
110+
echo " docker-compose.offline.yml"
111+
echo " .env (or .env.example — configure before starting)"
112+
echo " scripts/load-images.sh"
113+
echo ""
114+
echo "Then on the offline machine run:"
115+
echo " bash scripts/load-images.sh"
116+
echo " docker compose -f docker-compose.offline.yml up -d"

0 commit comments

Comments
 (0)