feat: Add flake and ragenix package generation and dev environment
This commit is contained in:
3
.envrc
Normal file
3
.envrc
Normal file
@@ -0,0 +1,3 @@
|
||||
# Automatically load the Nix development shell when entering this directory
|
||||
# Requires direnv: https://direnv.net/
|
||||
use flake
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -35,3 +35,7 @@ management-dashboard-web-app/users.txt
|
||||
|
||||
# Jupyter Notebooks
|
||||
*.ipynb
|
||||
# Nix
|
||||
result
|
||||
result-*
|
||||
.direnv/
|
||||
264
FLAKE_SETUP.md
Normal file
264
FLAKE_SETUP.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# USDA Vision - Nix Flake Setup
|
||||
|
||||
This directory now has a Nix flake for building and developing the USDA Vision system, with ragenix for managing secrets.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Development Environment
|
||||
|
||||
Enter the development shell with all tools:
|
||||
|
||||
```bash
|
||||
cd usda-vision
|
||||
nix develop
|
||||
```
|
||||
|
||||
This gives you:
|
||||
- Docker & Docker Compose
|
||||
- Node.js 20 with npm/pnpm
|
||||
- Python 3.11 with pip/virtualenv
|
||||
- Supabase CLI
|
||||
- Camera SDK (automatically in `LD_LIBRARY_PATH`)
|
||||
- ragenix for secrets management
|
||||
- All standard utilities (jq, yq, rsync, etc.)
|
||||
|
||||
### Building
|
||||
|
||||
Build the package:
|
||||
|
||||
```bash
|
||||
nix build
|
||||
# Or explicitly:
|
||||
nix build .#usda-vision
|
||||
```
|
||||
|
||||
Build just the camera SDK:
|
||||
|
||||
```bash
|
||||
nix build .#camera-sdk
|
||||
```
|
||||
|
||||
## Secrets Management with ragenix
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. **Generate or use an age key**:
|
||||
|
||||
```bash
|
||||
# Option 1: Generate a new age key
|
||||
mkdir -p ~/.config/age
|
||||
age-keygen -o ~/.config/age/keys.txt
|
||||
|
||||
# Option 2: Use your SSH key
|
||||
ssh-to-age < ~/.ssh/id_ed25519.pub
|
||||
# Copy the output to secrets/secrets.nix
|
||||
```
|
||||
|
||||
2. **Add your public key** to [secrets/secrets.nix](secrets/secrets.nix):
|
||||
|
||||
```nix
|
||||
{
|
||||
publicKeys = [
|
||||
"age1your_public_key_here"
|
||||
# or
|
||||
"ssh-ed25519 AAAA... user@host"
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
3. **Create encrypted environment files**:
|
||||
|
||||
```bash
|
||||
nix develop # Enter dev shell first
|
||||
ragenix -e secrets/env.age
|
||||
```
|
||||
|
||||
This opens your `$EDITOR` to edit the encrypted file. Add your environment variables:
|
||||
|
||||
```bash
|
||||
# Web environment (Vite)
|
||||
VITE_SUPABASE_URL=http://exp-dash:54321
|
||||
VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
||||
# ... etc
|
||||
```
|
||||
|
||||
For Azure OAuth:
|
||||
|
||||
```bash
|
||||
ragenix -e secrets/env.azure.age
|
||||
```
|
||||
|
||||
### Using Secrets in Development
|
||||
|
||||
In the development shell, you can:
|
||||
|
||||
```bash
|
||||
# Edit secrets
|
||||
ragenix -e secrets/env.age
|
||||
|
||||
# View decrypted content (careful in shared screens!)
|
||||
age -d -i ~/.config/age/keys.txt secrets/env.age
|
||||
|
||||
# Re-encrypt all secrets after adding a new public key
|
||||
ragenix -r
|
||||
```
|
||||
|
||||
### Using Secrets in Production (NixOS)
|
||||
|
||||
The flake includes a NixOS module that handles secrets automatically:
|
||||
|
||||
```nix
|
||||
# In your NixOS configuration
|
||||
{
|
||||
inputs.usda-vision.url = "path:/path/to/usda-vision";
|
||||
|
||||
# ... in your module:
|
||||
imports = [ inputs.usda-vision.nixosModules.default ];
|
||||
|
||||
services.usda-vision = {
|
||||
enable = true;
|
||||
secretsFile = config.age.secrets.usda-vision-env.path;
|
||||
};
|
||||
|
||||
# Configure ragenix/agenix to decrypt the secrets
|
||||
age.secrets.usda-vision-env = {
|
||||
file = inputs.usda-vision + "/secrets/env.age";
|
||||
mode = "0644";
|
||||
owner = "root";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
usda-vision/
|
||||
├── flake.nix # Flake definition with outputs
|
||||
├── package.nix # Main application build
|
||||
├── camera-sdk.nix # Camera SDK build
|
||||
├── secrets.nix # ragenix configuration
|
||||
├── secrets/
|
||||
│ ├── secrets.nix # Public keys
|
||||
│ ├── env.age # Encrypted .env (safe to commit)
|
||||
│ ├── env.azure.age # Encrypted Azure config (safe to commit)
|
||||
│ └── README.md # Secrets documentation
|
||||
└── ... (rest of the app)
|
||||
```
|
||||
|
||||
## Migration from Old Setup
|
||||
|
||||
### Old Workflow
|
||||
- Manual `.env` file management
|
||||
- Secrets in plaintext (git-ignored)
|
||||
- Build defined in parent `default.nix`
|
||||
|
||||
### New Workflow
|
||||
- Encrypted `.age` files in git
|
||||
- Secrets managed with ragenix
|
||||
- Self-contained flake in `usda-vision/`
|
||||
- Development shell with all tools
|
||||
|
||||
### Migration Steps
|
||||
|
||||
1. **Encrypt existing `.env` files**:
|
||||
|
||||
```bash
|
||||
cd usda-vision
|
||||
nix develop
|
||||
|
||||
# Setup your age key first (see above)
|
||||
|
||||
# Encrypt the main .env
|
||||
ragenix -e secrets/env.age
|
||||
# Paste contents of old .env file, save and exit
|
||||
|
||||
# Encrypt Azure config
|
||||
ragenix -e secrets/env.azure.age
|
||||
# Paste contents of old .env.azure file, save and exit
|
||||
```
|
||||
|
||||
2. **Delete unencrypted files** (they're git-ignored but still local):
|
||||
|
||||
```bash
|
||||
rm .env .env.azure management-dashboard-web-app/.env
|
||||
```
|
||||
|
||||
3. **Commit encrypted secrets**:
|
||||
|
||||
```bash
|
||||
git add secrets/env.age secrets/env.azure.age secrets/secrets.nix
|
||||
git commit -m "Add encrypted secrets with ragenix"
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### Security
|
||||
- ✅ Secrets encrypted at rest
|
||||
- ✅ Safe to commit to git
|
||||
- ✅ Key-based access control
|
||||
- ✅ Audit trail (git history)
|
||||
|
||||
### Development
|
||||
- ✅ Reproducible environment
|
||||
- ✅ All tools included
|
||||
- ✅ No manual setup
|
||||
- ✅ Version-locked dependencies
|
||||
|
||||
### Deployment
|
||||
- ✅ Declarative secrets management
|
||||
- ✅ Automatic decryption on NixOS
|
||||
- ✅ No manual key distribution
|
||||
- ✅ Clean integration with existing infrastructure
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Add a new developer
|
||||
|
||||
1. They generate an age key or use their SSH key
|
||||
2. They send you their public key
|
||||
3. You add it to `secrets/secrets.nix`
|
||||
4. Re-encrypt all secrets: `ragenix -r`
|
||||
5. Commit and push
|
||||
|
||||
### Rotate a secret
|
||||
|
||||
1. Edit the encrypted file: `ragenix -e secrets/env.age`
|
||||
2. Update the value
|
||||
3. Save and exit
|
||||
4. Commit: `git commit secrets/env.age -m "Rotate API key"`
|
||||
|
||||
### Build without flakes
|
||||
|
||||
If you need to build on a system without flakes enabled:
|
||||
|
||||
```bash
|
||||
nix-build -E '(import (fetchTarball https://github.com/edolstra/flake-compat/archive/master.tar.gz) { src = ./.; }).defaultNix.default'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "error: getting status of '...': No such file or directory"
|
||||
|
||||
Make sure you're in the `usda-vision` directory when running `nix develop` or `nix build`.
|
||||
|
||||
### "cannot decrypt: no valid identity"
|
||||
|
||||
Your age private key isn't found. Check:
|
||||
- `~/.config/age/keys.txt` exists
|
||||
- Your public key is in `secrets/secrets.nix`
|
||||
- You've run `ragenix -r` after adding your key
|
||||
|
||||
### "experimental feature 'flakes' not enabled"
|
||||
|
||||
Add to `~/.config/nix/nix.conf`:
|
||||
```
|
||||
experimental-features = nix-command flakes
|
||||
```
|
||||
|
||||
Or run with: `nix --experimental-features 'nix-command flakes' develop`
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Nix Flakes](https://nixos.wiki/wiki/Flakes)
|
||||
- [ragenix](https://github.com/yaxitech/ragenix)
|
||||
- [age encryption](https://github.com/FiloSottile/age)
|
||||
241
SETUP_COMPLETE.md
Normal file
241
SETUP_COMPLETE.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# USDA Vision - Flake Migration Complete ✅
|
||||
|
||||
## Summary
|
||||
|
||||
Your USDA Vision repository now has:
|
||||
|
||||
1. **Self-contained Nix flake** (`flake.nix`)
|
||||
- Independent build system
|
||||
- Development environment
|
||||
- NixOS module for deployment
|
||||
|
||||
2. **Encrypted secrets management** (ragenix)
|
||||
- `.age` files safe to commit to git
|
||||
- Key-based access control
|
||||
- No more plaintext `.env` files
|
||||
|
||||
3. **Modular build** (package.nix, camera-sdk.nix)
|
||||
- Cleaner organization
|
||||
- Easier to maintain
|
||||
- Reusable components
|
||||
|
||||
4. **Updated parent** (../default.nix)
|
||||
- Now references the flake
|
||||
- Removed 200+ lines of inline derivations
|
||||
|
||||
## Files Added
|
||||
|
||||
### Core Flake Files
|
||||
- ✅ `flake.nix` - Main flake definition with outputs
|
||||
- ✅ `package.nix` - Application build logic
|
||||
- ✅ `camera-sdk.nix` - Camera SDK build logic
|
||||
- ✅ `secrets.nix` - ragenix configuration
|
||||
|
||||
### Secrets Infrastructure
|
||||
- ✅ `secrets/secrets.nix` - Public key list
|
||||
- ✅ `secrets/README.md` - Secrets documentation
|
||||
- ✅ `secrets/.gitignore` - Protect plaintext files
|
||||
|
||||
### Documentation & Helpers
|
||||
- ✅ `FLAKE_SETUP.md` - Complete setup guide
|
||||
- ✅ `setup-dev.sh` - Interactive setup script
|
||||
- ✅ `.envrc` - direnv integration (optional)
|
||||
|
||||
### Parent Directory
|
||||
- ✅ `NIX_FLAKE_MIGRATION.md` - Migration summary
|
||||
|
||||
## Next Steps
|
||||
|
||||
### 1. Commit the Flake Files
|
||||
|
||||
The flake needs to be in git to work:
|
||||
|
||||
```bash
|
||||
cd /home/engr-ugaif/usda-dash-config/usda-vision
|
||||
|
||||
# Add all new flake files
|
||||
git add flake.nix package.nix camera-sdk.nix secrets.nix
|
||||
git add secrets/secrets.nix secrets/README.md secrets/.gitignore
|
||||
git add FLAKE_SETUP.md setup-dev.sh .envrc .gitignore
|
||||
|
||||
# Commit
|
||||
git commit -m "Add Nix flake with ragenix secrets management
|
||||
|
||||
- Self-contained flake build system
|
||||
- Development shell with all tools
|
||||
- ragenix for encrypted secrets
|
||||
- Modular package definitions
|
||||
"
|
||||
```
|
||||
|
||||
### 2. Set Up Your Age Key
|
||||
|
||||
```bash
|
||||
cd /home/engr-ugaif/usda-dash-config/usda-vision
|
||||
|
||||
# Option A: Use the interactive setup script
|
||||
./setup-dev.sh
|
||||
|
||||
# Option B: Manual setup
|
||||
mkdir -p ~/.config/age
|
||||
age-keygen -o ~/.config/age/keys.txt
|
||||
# Then add your public key to secrets/secrets.nix
|
||||
```
|
||||
|
||||
### 3. Encrypt Your Secrets
|
||||
|
||||
```bash
|
||||
# Enter the development environment
|
||||
nix develop
|
||||
|
||||
# Encrypt main .env file
|
||||
ragenix -e secrets/env.age
|
||||
# Paste your current .env contents, save, exit
|
||||
|
||||
# Encrypt Azure config
|
||||
ragenix -e secrets/env.azure.age
|
||||
# Paste your current .env.azure contents, save, exit
|
||||
|
||||
# Commit encrypted secrets
|
||||
git add secrets/env.age secrets/env.azure.age
|
||||
git commit -m "Add encrypted environment configuration"
|
||||
```
|
||||
|
||||
### 4. Test the Setup
|
||||
|
||||
```bash
|
||||
# Test that the build works
|
||||
nix build
|
||||
|
||||
# Test the development shell
|
||||
nix develop
|
||||
# You should see a welcome message
|
||||
|
||||
# Inside the dev shell, verify tools
|
||||
docker-compose --version
|
||||
supabase --version
|
||||
ragenix --help
|
||||
```
|
||||
|
||||
### 5. Update the Parent Repository
|
||||
|
||||
```bash
|
||||
cd /home/engr-ugaif/usda-dash-config
|
||||
|
||||
# Commit the updated default.nix
|
||||
git add default.nix NIX_FLAKE_MIGRATION.md
|
||||
git commit -m "Update default.nix to use usda-vision flake
|
||||
|
||||
- Removed inline derivations
|
||||
- Now references usda-vision flake packages
|
||||
- Cleaner, more maintainable code
|
||||
"
|
||||
```
|
||||
|
||||
### 6. Clean Up Old Files (Optional)
|
||||
|
||||
After verifying everything works, you can delete the old plaintext secrets:
|
||||
|
||||
```bash
|
||||
cd /home/engr-ugaif/usda-dash-config/usda-vision
|
||||
|
||||
# These are already git-ignored, but remove them locally
|
||||
rm -f .env .env.azure management-dashboard-web-app/.env
|
||||
|
||||
echo "✅ Old plaintext secrets removed"
|
||||
```
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
- [ ] Flake files committed to git
|
||||
- [ ] Age key generated at `~/.config/age/keys.txt`
|
||||
- [ ] Public key added to `secrets/secrets.nix`
|
||||
- [ ] Secrets encrypted and committed
|
||||
- [ ] `nix build` succeeds
|
||||
- [ ] `nix develop` works
|
||||
- [ ] Parent `default.nix` updated and committed
|
||||
- [ ] Old `.env` files deleted
|
||||
|
||||
## Usage Quick Reference
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Enter dev environment (one-time per session)
|
||||
cd usda-vision
|
||||
nix develop
|
||||
|
||||
# Edit secrets
|
||||
ragenix -e secrets/env.age
|
||||
|
||||
# Normal docker-compose workflow
|
||||
docker-compose up -d
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Build everything
|
||||
nix build
|
||||
|
||||
# Build specific packages
|
||||
nix build .#usda-vision
|
||||
nix build .#camera-sdk
|
||||
```
|
||||
|
||||
### Secrets Management
|
||||
|
||||
```bash
|
||||
# Edit encrypted secret
|
||||
ragenix -e secrets/env.age
|
||||
|
||||
# Re-key after adding a new public key
|
||||
ragenix -r
|
||||
|
||||
# View decrypted (careful!)
|
||||
age -d -i ~/.config/age/keys.txt secrets/env.age
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "cannot decrypt: no valid identity"
|
||||
|
||||
Your age key isn't configured. Run:
|
||||
```bash
|
||||
./setup-dev.sh
|
||||
```
|
||||
|
||||
### "error: flake.nix is not in git"
|
||||
|
||||
Commit the flake files:
|
||||
```bash
|
||||
git add flake.nix package.nix camera-sdk.nix secrets.nix
|
||||
git commit -m "Add flake files"
|
||||
```
|
||||
|
||||
### "experimental feature 'flakes' not enabled"
|
||||
|
||||
Add to `~/.config/nix/nix.conf`:
|
||||
```
|
||||
experimental-features = nix-command flakes
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- **Full Setup Guide**: [FLAKE_SETUP.md](FLAKE_SETUP.md)
|
||||
- **Secrets Guide**: [secrets/README.md](secrets/README.md)
|
||||
- **Migration Summary**: [../NIX_FLAKE_MIGRATION.md](../NIX_FLAKE_MIGRATION.md)
|
||||
|
||||
## Questions?
|
||||
|
||||
Refer to [FLAKE_SETUP.md](FLAKE_SETUP.md) for detailed documentation, or run:
|
||||
|
||||
```bash
|
||||
./setup-dev.sh # Interactive setup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Migration completed on**: 2026-01-30
|
||||
**Created by**: GitHub Copilot
|
||||
44
camera-sdk.nix
Normal file
44
camera-sdk.nix
Normal file
@@ -0,0 +1,44 @@
|
||||
{ stdenv
|
||||
, lib
|
||||
, makeWrapper
|
||||
, libusb1
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
pname = "mindvision-camera-sdk";
|
||||
version = "2.1.0.49";
|
||||
|
||||
# Use the camera_sdk directory as source
|
||||
src = ./camera-management-api/camera_sdk;
|
||||
|
||||
nativeBuildInputs = [ makeWrapper ];
|
||||
buildInputs = [ libusb1 ];
|
||||
|
||||
unpackPhase = ''
|
||||
cp -r $src/* .
|
||||
tar xzf "linuxSDK_V2.1.0.49(250108).tar.gz"
|
||||
cd "linuxSDK_V2.1.0.49(250108)"
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/lib $out/include
|
||||
|
||||
# Copy x64 library files (SDK has arch-specific subdirs)
|
||||
if [ -d lib/x64 ]; then
|
||||
cp -r lib/x64/* $out/lib/ || true
|
||||
fi
|
||||
|
||||
# Copy header files
|
||||
if [ -d include ]; then
|
||||
cp -r include/* $out/include/ || true
|
||||
fi
|
||||
|
||||
# Make libraries executable
|
||||
chmod +x $out/lib/*.so* 2>/dev/null || true
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "MindVision Camera SDK";
|
||||
platforms = platforms.linux;
|
||||
};
|
||||
}
|
||||
239
flake.lock
generated
Normal file
239
flake.lock
generated
Normal file
@@ -0,0 +1,239 @@
|
||||
{
|
||||
"nodes": {
|
||||
"agenix": {
|
||||
"inputs": {
|
||||
"darwin": "darwin",
|
||||
"home-manager": "home-manager",
|
||||
"nixpkgs": [
|
||||
"ragenix",
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1761656077,
|
||||
"narHash": "sha256-lsNWuj4Z+pE7s0bd2OKicOFq9bK86JE0ZGeKJbNqb94=",
|
||||
"owner": "ryantm",
|
||||
"repo": "agenix",
|
||||
"rev": "9ba0d85de3eaa7afeab493fed622008b6e4924f5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ryantm",
|
||||
"repo": "agenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1760924934,
|
||||
"narHash": "sha256-tuuqY5aU7cUkR71sO2TraVKK2boYrdW3gCSXUkF4i44=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "c6b4d5308293d0d04fcfeee92705017537cad02f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"darwin": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"ragenix",
|
||||
"agenix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1744478979,
|
||||
"narHash": "sha256-dyN+teG9G82G+m+PX/aSAagkC+vUv0SgUw3XkPhQodQ=",
|
||||
"owner": "lnl7",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "43975d782b418ebf4969e9ccba82466728c2851b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "lnl7",
|
||||
"ref": "master",
|
||||
"repo": "nix-darwin",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"ragenix",
|
||||
"agenix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1745494811,
|
||||
"narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1769461804,
|
||||
"narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"ragenix": {
|
||||
"inputs": {
|
||||
"agenix": "agenix",
|
||||
"crane": "crane",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-overlay": "rust-overlay"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1761832913,
|
||||
"narHash": "sha256-VCNVjjuRvrKPiYYwqhE3BAKIaReiKXGpxGp27lZ0MFM=",
|
||||
"owner": "yaxitech",
|
||||
"repo": "ragenix",
|
||||
"rev": "83bccfdea758241999f32869fb6b36f7ac72f1ac",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "yaxitech",
|
||||
"repo": "ragenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"ragenix": "ragenix"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"ragenix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1761791894,
|
||||
"narHash": "sha256-myRIDh+PxaREz+z9LzbqBJF+SnTFJwkthKDX9zMyddY=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "59c45eb69d9222a4362673141e00ff77842cd219",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
176
flake.nix
Normal file
176
flake.nix
Normal file
@@ -0,0 +1,176 @@
|
||||
{
|
||||
description = "USDA Vision camera management system";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
# For secrets management
|
||||
ragenix = {
|
||||
url = "github:yaxitech/ragenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, ragenix }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
|
||||
# Import our package definition
|
||||
usda-vision-package = pkgs.callPackage ./package.nix { };
|
||||
camera-sdk = pkgs.callPackage ./camera-sdk.nix { };
|
||||
|
||||
in
|
||||
{
|
||||
packages = {
|
||||
default = usda-vision-package;
|
||||
usda-vision = usda-vision-package;
|
||||
camera-sdk = camera-sdk;
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
name = "usda-vision-dev";
|
||||
|
||||
# Input packages for the development shell
|
||||
buildInputs = with pkgs; [
|
||||
# Core development tools
|
||||
git
|
||||
vim
|
||||
curl
|
||||
wget
|
||||
|
||||
# Docker for local development
|
||||
docker
|
||||
docker-compose
|
||||
|
||||
# Supabase CLI
|
||||
supabase-cli
|
||||
|
||||
# Node.js for web app development
|
||||
nodejs_20
|
||||
nodePackages.npm
|
||||
nodePackages.pnpm
|
||||
|
||||
# Python for camera API
|
||||
python311
|
||||
python311Packages.pip
|
||||
python311Packages.virtualenv
|
||||
|
||||
# Camera SDK
|
||||
camera-sdk
|
||||
|
||||
# Secrets management
|
||||
ragenix.packages.${system}.default
|
||||
age
|
||||
ssh-to-age
|
||||
|
||||
# Utilities
|
||||
jq
|
||||
yq
|
||||
rsync
|
||||
gnused
|
||||
gawk
|
||||
];
|
||||
|
||||
# Environment variables for development
|
||||
shellHook = ''
|
||||
export LD_LIBRARY_PATH="${camera-sdk}/lib:$LD_LIBRARY_PATH"
|
||||
export CAMERA_SDK_PATH="${camera-sdk}"
|
||||
|
||||
# Set up Python virtual environment
|
||||
if [ ! -d .venv ]; then
|
||||
echo "Creating Python virtual environment..."
|
||||
python -m venv .venv
|
||||
fi
|
||||
|
||||
echo "USDA Vision Development Environment"
|
||||
echo "===================================="
|
||||
echo "Camera SDK: ${camera-sdk}"
|
||||
echo ""
|
||||
echo "Available commands:"
|
||||
echo " - docker-compose: Manage containers"
|
||||
echo " - supabase: Supabase CLI"
|
||||
echo " - ragenix: Manage encrypted secrets"
|
||||
echo " - age: Encrypt/decrypt files"
|
||||
echo ""
|
||||
echo "To activate Python venv: source .venv/bin/activate"
|
||||
echo "To edit secrets: ragenix -e secrets/env.age"
|
||||
echo ""
|
||||
'';
|
||||
|
||||
# Additional environment configuration
|
||||
DOCKER_BUILDKIT = "1";
|
||||
COMPOSE_DOCKER_CLI_BUILD = "1";
|
||||
};
|
||||
|
||||
# NixOS module for easy integration
|
||||
nixosModules.default = { config, lib, ... }: {
|
||||
options.services.usda-vision = {
|
||||
enable = lib.mkEnableOption "USDA Vision camera management system";
|
||||
|
||||
secretsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = "Path to the ragenix-managed secrets file";
|
||||
};
|
||||
|
||||
dataDir = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/var/lib/usda-vision";
|
||||
description = "Directory for USDA Vision application data";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf config.services.usda-vision.enable {
|
||||
environment.systemPackages = [
|
||||
usda-vision-package
|
||||
camera-sdk
|
||||
pkgs.docker-compose
|
||||
];
|
||||
|
||||
environment.variables.LD_LIBRARY_PATH = "${camera-sdk}/lib";
|
||||
|
||||
virtualisation.docker = {
|
||||
enable = true;
|
||||
autoPrune.enable = true;
|
||||
};
|
||||
|
||||
systemd.services.usda-vision = {
|
||||
description = "USDA Vision Docker Compose Stack";
|
||||
after = [ "docker.service" "network-online.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
preStart = ''
|
||||
# Sync application code
|
||||
${pkgs.rsync}/bin/rsync -av --delete \
|
||||
--checksum \
|
||||
--exclude='node_modules' \
|
||||
--exclude='.env' \
|
||||
--exclude='__pycache__' \
|
||||
--exclude='.venv' \
|
||||
${usda-vision-package}/opt/usda-vision/ ${config.services.usda-vision.dataDir}/
|
||||
|
||||
# Copy secrets if managed by ragenix
|
||||
if [ -f "${config.services.usda-vision.secretsFile}" ]; then
|
||||
cp "${config.services.usda-vision.secretsFile}" ${config.services.usda-vision.dataDir}/.env
|
||||
fi
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
WorkingDirectory = config.services.usda-vision.dataDir;
|
||||
ExecStart = "${pkgs.docker-compose}/bin/docker-compose up -d --build";
|
||||
ExecStop = "${pkgs.docker-compose}/bin/docker-compose down";
|
||||
TimeoutStartSec = 300;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
131
package.nix
Normal file
131
package.nix
Normal file
@@ -0,0 +1,131 @@
|
||||
{ lib
|
||||
, stdenv
|
||||
, makeWrapper
|
||||
, rsync
|
||||
, gnused
|
||||
, gawk
|
||||
, docker-compose
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
pname = "usda-vision";
|
||||
version = "1.0.0";
|
||||
|
||||
# Use the directory from this repository with explicit source filtering
|
||||
src = lib.cleanSourceWith {
|
||||
src = ./.;
|
||||
filter = path: type:
|
||||
let
|
||||
baseName = baseNameOf path;
|
||||
in
|
||||
# Exclude git, but include everything else
|
||||
baseName != ".git" &&
|
||||
baseName != ".cursor" &&
|
||||
baseName != "__pycache__" &&
|
||||
baseName != "node_modules" &&
|
||||
baseName != ".venv" &&
|
||||
baseName != ".age" &&
|
||||
baseName != "flake.nix" &&
|
||||
baseName != "flake.lock" &&
|
||||
baseName != "package.nix" &&
|
||||
baseName != "camera-sdk.nix";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [ makeWrapper rsync ];
|
||||
|
||||
# Don't run these phases, we'll do everything in installPhase
|
||||
dontBuild = true;
|
||||
dontConfigure = true;
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/opt/usda-vision
|
||||
|
||||
# Debug: show what's in source
|
||||
echo "Source directory contents:"
|
||||
ls -la $src/ || true
|
||||
|
||||
# Process docker-compose.yml - replace paths and configure SDK from Nix
|
||||
if [ -f $src/docker-compose.yml ]; then
|
||||
# Basic path replacements with sed
|
||||
${gnused}/bin/sed \
|
||||
-e 's|env_file:.*management-dashboard-web-app/\.env|env_file: /var/lib/usda-vision/.env|g' \
|
||||
-e 's|\./management-dashboard-web-app/\.env|/var/lib/usda-vision/.env|g' \
|
||||
-e 's|\./management-dashboard-web-app|/var/lib/usda-vision/management-dashboard-web-app|g' \
|
||||
-e 's|\./media-api|/var/lib/usda-vision/media-api|g' \
|
||||
-e 's|\./video-remote|/var/lib/usda-vision/video-remote|g' \
|
||||
-e 's|\./scheduling-remote|/var/lib/usda-vision/scheduling-remote|g' \
|
||||
-e 's|\./vision-system-remote|/var/lib/usda-vision/vision-system-remote|g' \
|
||||
-e 's|\./camera-management-api|/var/lib/usda-vision/camera-management-api|g' \
|
||||
$src/docker-compose.yml > $TMPDIR/docker-compose-step1.yml
|
||||
|
||||
# Remove SDK installation blocks using awk for better multi-line handling
|
||||
${gawk}/bin/awk '
|
||||
/# Only install system packages if not already installed/ { skip=1 }
|
||||
skip && /^ fi$/ { skip=0; next }
|
||||
/# Install camera SDK if not already installed/ { skip_sdk=1 }
|
||||
skip_sdk && /^ fi;$/ { skip_sdk=0; next }
|
||||
!skip && !skip_sdk { print }
|
||||
' $TMPDIR/docker-compose-step1.yml > $TMPDIR/docker-compose.yml
|
||||
|
||||
rm -f $TMPDIR/docker-compose-step1.yml
|
||||
fi
|
||||
|
||||
# Copy all application files using rsync with chmod
|
||||
${rsync}/bin/rsync -av \
|
||||
--chmod=Du+w \
|
||||
--exclude='.git' \
|
||||
--exclude='docker-compose.yml' \
|
||||
--exclude='.env' \
|
||||
--exclude='*.age' \
|
||||
--exclude='flake.nix' \
|
||||
--exclude='flake.lock' \
|
||||
--exclude='package.nix' \
|
||||
--exclude='camera-sdk.nix' \
|
||||
--exclude='management-dashboard-web-app/.env' \
|
||||
$src/ $out/opt/usda-vision/
|
||||
|
||||
# Copy the processed docker-compose.yml
|
||||
if [ -f $TMPDIR/docker-compose.yml ]; then
|
||||
cp $TMPDIR/docker-compose.yml $out/opt/usda-vision/docker-compose.yml
|
||||
fi
|
||||
|
||||
# Verify files were copied
|
||||
echo "Destination directory contents:"
|
||||
ls -la $out/opt/usda-vision/ || true
|
||||
|
||||
# Create convenience scripts
|
||||
mkdir -p $out/bin
|
||||
|
||||
cat > $out/bin/usda-vision-start <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
cd $out/opt/usda-vision
|
||||
${docker-compose}/bin/docker-compose up -d --build
|
||||
EOF
|
||||
|
||||
cat > $out/bin/usda-vision-stop <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
cd $out/opt/usda-vision
|
||||
${docker-compose}/bin/docker-compose down
|
||||
EOF
|
||||
|
||||
cat > $out/bin/usda-vision-logs <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
cd $out/opt/usda-vision
|
||||
${docker-compose}/bin/docker-compose logs -f "$@"
|
||||
EOF
|
||||
|
||||
cat > $out/bin/usda-vision-restart <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
cd $out/opt/usda-vision
|
||||
${docker-compose}/bin/docker-compose restart "$@"
|
||||
EOF
|
||||
|
||||
chmod +x $out/bin/usda-vision-*
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "USDA Vision camera management system";
|
||||
maintainers = [ "UGA Innovation Factory" ];
|
||||
platforms = platforms.linux;
|
||||
};
|
||||
}
|
||||
14
secrets.nix
Normal file
14
secrets.nix
Normal file
@@ -0,0 +1,14 @@
|
||||
# Ragenix Configuration
|
||||
# This file defines which secrets to manage and their permissions
|
||||
|
||||
let
|
||||
# Import public keys from secrets.nix
|
||||
keys = import ./secrets.nix;
|
||||
in
|
||||
{
|
||||
# Main environment file
|
||||
"env.age".publicKeys = keys.publicKeys;
|
||||
|
||||
# Azure OAuth configuration
|
||||
"env.azure.age".publicKeys = keys.publicKeys;
|
||||
}
|
||||
11
secrets/.gitignore
vendored
Normal file
11
secrets/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Ignore unencrypted secrets
|
||||
*.env
|
||||
!*.env.example
|
||||
.env.*
|
||||
!.env.*.example
|
||||
|
||||
# Ignore age private keys (if accidentally placed here)
|
||||
*.txt
|
||||
|
||||
# Keep encrypted files
|
||||
!*.age
|
||||
75
secrets/README.md
Normal file
75
secrets/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# USDA Vision Secrets Management
|
||||
|
||||
This directory contains encrypted secrets managed by [ragenix](https://github.com/yaxitech/ragenix).
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Generate an age key** (if you don't have one):
|
||||
```bash
|
||||
# Generate a new age key
|
||||
age-keygen -o ~/.config/age/keys.txt
|
||||
|
||||
# Or convert your SSH key
|
||||
ssh-to-age < ~/.ssh/id_ed25519.pub
|
||||
```
|
||||
|
||||
2. **Add your public key to `secrets.nix`**:
|
||||
```nix
|
||||
{
|
||||
publicKeys = [
|
||||
"age1..." # Your age public key
|
||||
"ssh-ed25519 ..." # Or your SSH public key
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
3. **Create and encrypt environment files**:
|
||||
```bash
|
||||
# Create the encrypted .env file
|
||||
ragenix -e secrets/env.age
|
||||
|
||||
# Create the encrypted .env.azure file
|
||||
ragenix -e secrets/env.azure.age
|
||||
```
|
||||
|
||||
## Usage in Development
|
||||
|
||||
In the development shell:
|
||||
```bash
|
||||
# Edit encrypted secrets
|
||||
ragenix -e secrets/env.age
|
||||
|
||||
# Re-key secrets after adding a new public key
|
||||
ragenix -r
|
||||
```
|
||||
|
||||
## Usage in NixOS
|
||||
|
||||
The flake's NixOS module automatically handles decryption:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.usda-vision = {
|
||||
enable = true;
|
||||
secretsFile = config.age.secrets.usda-vision-env.path;
|
||||
};
|
||||
|
||||
age.secrets.usda-vision-env = {
|
||||
file = ./usda-vision/secrets/env.age;
|
||||
mode = "0644";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `secrets.nix` - Public keys configuration
|
||||
- `env.age` - Encrypted main .env file
|
||||
- `env.azure.age` - Encrypted Azure OAuth configuration
|
||||
- `README.md` - This file
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Never commit unencrypted `.env` files
|
||||
- Keep your age private key secure (`~/.config/age/keys.txt`)
|
||||
- The `.age` encrypted files are safe to commit to git
|
||||
14
secrets/secrets.nix
Normal file
14
secrets/secrets.nix
Normal file
@@ -0,0 +1,14 @@
|
||||
# Public keys for secret encryption
|
||||
# Add your age or SSH public keys here
|
||||
{
|
||||
publicKeys = [
|
||||
# Example age public key:
|
||||
# "age1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs3ekg8p"
|
||||
|
||||
# Example SSH public key (ed25519):
|
||||
# "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... user@host"
|
||||
|
||||
# Add your keys below:
|
||||
# TODO: Add your age or SSH public keys
|
||||
];
|
||||
}
|
||||
77
setup-dev.sh
Executable file
77
setup-dev.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
# Quick setup script for USDA Vision development
|
||||
|
||||
set -e
|
||||
|
||||
echo "======================================"
|
||||
echo "USDA Vision - Quick Setup"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Check if we're in the right directory
|
||||
if [ ! -f "flake.nix" ]; then
|
||||
echo "❌ Error: Must run from usda-vision directory"
|
||||
echo " cd to the directory containing flake.nix"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for age key
|
||||
if [ ! -f "$HOME/.config/age/keys.txt" ]; then
|
||||
echo "📝 No age key found at ~/.config/age/keys.txt"
|
||||
echo ""
|
||||
read -p "Would you like to generate one? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
mkdir -p "$HOME/.config/age"
|
||||
age-keygen -o "$HOME/.config/age/keys.txt"
|
||||
echo "✅ Age key generated!"
|
||||
echo ""
|
||||
else
|
||||
echo "❌ Cannot proceed without an age key"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get public key
|
||||
AGE_PUBLIC_KEY=$(grep "public key:" "$HOME/.config/age/keys.txt" | cut -d: -f2 | xargs)
|
||||
|
||||
echo "Your age public key is:"
|
||||
echo " $AGE_PUBLIC_KEY"
|
||||
echo ""
|
||||
|
||||
# Check if key is already in secrets.nix
|
||||
if grep -q "$AGE_PUBLIC_KEY" secrets/secrets.nix 2>/dev/null; then
|
||||
echo "✅ Your key is already in secrets/secrets.nix"
|
||||
else
|
||||
echo "⚠️ Your key is NOT in secrets/secrets.nix"
|
||||
echo ""
|
||||
read -p "Would you like to add it now? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
# Backup original
|
||||
cp secrets/secrets.nix secrets/secrets.nix.backup
|
||||
|
||||
# Add the key
|
||||
sed -i "/publicKeys = \[/a\ \"$AGE_PUBLIC_KEY\"" secrets/secrets.nix
|
||||
|
||||
echo "✅ Key added to secrets/secrets.nix"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "======================================"
|
||||
echo "Setup complete! Next steps:"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "1. Enter development environment:"
|
||||
echo " $ nix develop"
|
||||
echo ""
|
||||
echo "2. Create/edit encrypted secrets:"
|
||||
echo " $ ragenix -e secrets/env.age"
|
||||
echo " $ ragenix -e secrets/env.azure.age"
|
||||
echo ""
|
||||
echo "3. Start development:"
|
||||
echo " $ docker-compose up -d"
|
||||
echo ""
|
||||
echo "For more information, see FLAKE_SETUP.md"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user