Local development stack for Center for Open Neuroscience self-hosted
infrastructure, using podman-compose (or docker compose). This repo was
created as one of the subprojects of the https://github.com/con/serve hence
potentiall references to projects/ would refer to
https://github.com/con/serve/tree/master/projects . And originally folder was named liab-conserve/.
This compose setup reproduces the service patterns from
liab-deployments,
which uses pyinfra to deploy the same stack on bare Debian servers with
rootless Podman and systemd user units. See
con/serve/projects/liab-deployments.CLAUDE.md
for the frozen frontier reference.
- Container isolation: Claude Code runs in unprivileged podman — no capabilities, no device access
- SSH-only bridge: Only connection to VMs is SSH with Vagrant-generated keys
- VM ephemerality:
vagrant destroyresets to clean state - No host access: Container cannot reach host filesystem beyond mounted project dir
- No container management: No podman socket, can't affect other host containers
| Service | Description | Internal port |
|---|---|---|
| Forgejo-aneksajo | Git hosting with git-annex support | :3000 (behind go-away) |
| go-away | Bot/scraper protection reverse proxy | :8080 |
| HedgeDoc | Collaborative markdown editor | :8081 (mapped from :3000) |
Browser
│
├─ :8080 ──→ go-away ──→ forgejo (:3000 internal)
│ (bot protection, PoW challenges)
│
└─ :8081 ──→ hedgedoc (:3000 internal)
All three services share a conserve-net bridge network and communicate by container name.
Run the stack directly on the host with podman or docker.
# Start all services
podman-compose up -d
# Wait for everything to be healthy
podman-compose ps
# Provision CON user accounts
./scripts/provision-users.sh
# Access services
# Forgejo: http://localhost:8080 (may show go-away PoW challenge)
# HedgeDoc: http://localhost:8081
# Stop
podman-compose down
# Stop and remove all data
podman-compose down -vFor use with Claude Code or other AI-assisted development. Claude Code runs in an unprivileged container; the service stack runs in Vagrant/libvirt VMs on the host. The only bridge is SSH.
┌────────────────────────────────────────────────────────┐
│ HOST MACHINE │
│ │
│ ┌──────────────────────┐ ┌────────────────────────┐ │
│ │ Claude Code │ │ Vagrant VMs (libvirt) │ │
│ │ DevContainer │ │ │ │
│ │ (unprivileged) │ │ ┌──────────────────┐ │ │
│ │ │SSH│ │ debian-bookworm │ │ │
│ │ - code editing │──┼─│ :2222→:22 │ │ │
│ │ - ssh client │ │ │ podman-compose │ │ │
│ │ - pyinfra │ │ └──────────────────┘ │ │
│ │ │ │ ┌──────────────────┐ │ │
│ │ │SSH│ │ ubuntu-noble │ │ │
│ │ │──┼─│ :2223→:22 │ │ │
│ └──────────────────────┘ │ └──────────────────┘ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ rocky9 │ │ │
│ │ │ :2224→:22 │ │ │
│ │ └──────────────────┘ │ │
│ └────────────────────────┘ │
└────────────────────────────────────────────────────────┘
| Machine | Box | SSH port | go-away | Forgejo direct | HedgeDoc |
|---|---|---|---|---|---|
| debian-bookworm | debian/bookworm64 | 2222 | :18080 | :13000 | :18081 |
| ubuntu-noble | cloud-image/ubuntu-24.04 | 2223 | :28080 | :23000 | :28081 |
| rocky9 | generic/rocky9 | 2224 | :38080 | :33000 | :38081 |
# Start a VM (all are autostart: false)
vagrant up debian-bookworm
# Generate SSH config for DevContainer access
./scripts/vagrant-setup.sh
# Direct SSH from host also works
vagrant ssh debian-bookworm# Test connectivity
ssh -F .vagrant/ssh-config debian-bookworm hostname
# Full deploy: sync files, start services, provision users
./scripts/provision-vm.sh debian-bookworm
# Or deploy manually with pyinfra
pyinfra --ssh-user vagrant --ssh-key .vagrant/machines/debian-bookworm/libvirt/private_key \
--sudo <VM-IP> deploy.py
# Access services
curl http://host.containers.internal:18080 # Forgejo via go-away
curl http://host.containers.internal:18081 # HedgeDoc# From host
vagrant destroy -f debian-bookworm # remove one VM
vagrant destroy -f # remove all VMsBoth Forgejo and HedgeDoc use GitHub OAuth for login — no local password management needed.
-
Copy the environment template and fill in secrets:
cp .env.template .env # edit .env — see comments inside for instructions -
Create two GitHub OAuth Apps at https://github.com/settings/developers:
App Homepage URL Callback URL Forgejo http://forgejo.conserve.local:8080http://forgejo.conserve.local:8080/user/oauth2/github/callbackHedgeDoc http://hedgedoc.conserve.local:8081http://hedgedoc.conserve.local:8081/auth/github/callback -
Put the Client ID / Secret values into
.env(see.env.templatefor variable names). -
Add to
/etc/hosts(pointing to the VM IP or127.0.0.1):192.168.121.60 forgejo.conserve.local hedgedoc.conserve.local
- Forgejo: http://forgejo.conserve.local:8080 — click "Sign in with github"
- Forgejo direct (bypasses go-away): http://forgejo.conserve.local:3000
- HedgeDoc: http://hedgedoc.conserve.local:8081 — click "Sign in via GitHub"
All config files are in config/ and mounted into containers.
| File | Purpose |
|---|---|
config/forgejo/app.ini |
Forgejo server settings (SQLite, no SSH, GitHub OAuth enabled) |
config/go-away/policy.yml |
Bot protection rules (pass-through for dev) |
config/hedgedoc/config.json |
HedgeDoc settings (SQLite, GitHub OAuth, local image uploads) |
config/con-users.tsv |
CON member accounts to provision |
Config files contain CHANGE-ME placeholders for secrets. Real values are
provided at runtime via environment variables from .env (see .env.template).
Forgejo secrets are overridden via FORGEJO__section__KEY env vars;
HedgeDoc secrets via CMD_* env vars.
scripts/provision-users.sh reads config/con-users.tsv and creates Forgejo accounts.
- Generates random initial passwords with
--must-change-password=true yarikopticis created as admin- Skips users that already exist
- Emeriti are commented out in the TSV; uncomment to provision
For domain-based access matching production, add to /etc/hosts:
127.0.0.1 forgejo.conserve.local hedgedoc.conserve.local