Configuration
Reference for every setting in the self-hosted .env file, covering hostnames, ports, secrets, databases, storage, SMTP email, the first administrator, the license and AI.
All configuration lives in .env, in the directory that holds docker-compose.yml. docker/generate-env.sh creates it from .env.self-hosted.example, which documents every variable. Docker Compose reads .env to fill in docker-compose.yml, and passes it to the app container as its environment.
Editing .env
- An empty value means the setting is not set.
${NAME}refers to another variable. For example,NEXTAUTH_URL=https://${APP_HOST}followsAPP_HOST, so a hostname only needs to change in one place.- Wrap values that contain
$,#or spaces in single quotes, for exampleSMTP_PASSWORD='p@ss#word$1'. The setup script does this for the administrator password and the SMTP credentials it writes. - Flags take the values
trueorfalse.
Applying Changes
After editing .env, recreate the containers whose settings changed:
docker compose --profile tls up -d # with the bundled Caddy
cd /opt/vulnsy && sudo docker compose up -d # with your own reverse proxydocker compose restart is not enough: it restarts the containers with the environment they were created with.
In the tables below, Default is the value that generate-env.sh writes, and After install says whether you can change the setting later.
Edition and Image
| Variable | Meaning | Default | After install |
|---|---|---|---|
VULNSY_EDITION | Selects the self-hosted edition | self-hosted | Do not change |
VULNSY_VERSION | Version of the installed package; set by install.sh from the package's VERSION file. Do not edit by hand: install.sh upgrade updates it. | Yes, by upgrading | |
COMPOSE_PROFILES | tls to run the bundled Caddy (the default), empty when you run your own reverse proxy (--no-tls). | Yes |
Hostnames
| Variable | Meaning | Default | After install |
|---|---|---|---|
APP_HOST | Public hostname of the app, without https:// | Set by the script | Yes. Update DNS first, and your certificate if you provide your own. Links in emails that were already sent keep the old hostname. |
FILES_HOST | Public hostname for file downloads, served by MinIO. Must differ from APP_HOST. | files. followed by the app host | Yes, as for APP_HOST |
NEXTAUTH_URL | Public URL of the app, used for sign-in callbacks and links | https://${APP_HOST} | Follows APP_HOST |
NEXT_PUBLIC_APP_URL | Public URL of the app, used for links | https://${APP_HOST} | Follows APP_HOST |
Published Ports
| Variable | Meaning | Default | After install |
|---|---|---|---|
BIND_ADDRESS | Server address on which the app and MinIO ports are published | 127.0.0.1 | Yes |
APP_PORT | Host port of the app. Your own reverse proxy forwards the app host here. | 3000 | Yes |
MINIO_PORT | Host port of MinIO's S3 API. Your own reverse proxy forwards the files host here. | 9000 | Yes |
With the bundled Caddy, only ports 80 and 443 need to be reachable. Change BIND_ADDRESS to 0.0.0.0, or to one of the server's addresses, only when your reverse proxy runs on another machine.
Secrets
| Variable | Meaning | Default | After install |
|---|---|---|---|
NEXTAUTH_SECRET | Signs user sessions | Generated | Changing it signs everyone out |
AUTH_SECRET | The same secret, under the name that newer sign-in code reads | ${NEXTAUTH_SECRET} | Leave as it is |
PLATFORM_ADMIN_JWT_SECRET | Signs sessions of the vulnsy.com operator portal, which the self-hosted edition does not serve | Generated | Yes, with no effect on users |
API_KEY_PEPPER | Secret used to hash REST API keys | Generated | No, see below |
AI_SETTINGS_ENCRYPTION_KEY | Encrypts stored AI credentials. Exactly 64 hexadecimal characters. | Generated | No, see below |
Three secrets cannot change without consequences. The same applies when you restore the database with a different .env:
| Secret | If it changes | To recover |
|---|---|---|
NEXTAUTH_SECRET | Every user is signed out | Users sign in again |
API_KEY_PEPPER | Every existing REST API key stops working | Users create new API keys |
AI_SETTINGS_ENCRYPTION_KEY | Stored AI credentials (an OpenRouter key or an endpoint token) can no longer be decrypted | An administrator enters them again under Admin > Organization > AI Assistant |
Back up .env together with the databases, and restore them together. See Backup & Restore.
PostgreSQL
| Variable | Meaning | Default | After install |
|---|---|---|---|
POSTGRES_USER | Database role that owns the three Vulnsy databases | vulnsy | No. It is set when the database volume is first created. |
POSTGRES_PASSWORD | Password of that role. Use URL-safe characters only, because it is part of the database URLs. | Generated (hexadecimal) | Not by editing .env alone, see below |
CONTROL_PLANE_DATABASE_URL | Connection URL of vulnsy_control: installation settings, license, instance ID and AI settings | Built from the values above | Leave as it is |
SHARED_DATABASE_URL | Connection URL of vulnsy_shared: the Vulnsy library | Built from the values above | Leave as it is |
TENANT_DATABASE_URL | Connection URL of vulnsy_tenant: your organization's users and data | Built from the values above | Leave as it is |
Changing the Database Password
PostgreSQL reads POSTGRES_PASSWORD only when it creates its data volume, on the first start. To change the password later, change it in PostgreSQL first:
docker compose exec postgres psql -U vulnsy -d postgres -c '\password vulnsy'Enter a new password made of letters and digits only. Then set the same value as POSTGRES_PASSWORD in .env, and run docker compose up -d. If you changed POSTGRES_USER before the first start, use that name instead of vulnsy.
Object Storage
| Variable | Meaning | Default | After install |
|---|---|---|---|
MINIO_ROOT_USER | MinIO administrator user. The app uses it as its S3 access key. | vulnsy | Keep the value |
MINIO_ROOT_PASSWORD | MinIO administrator password, at least 8 characters. The app uses it as its S3 secret key. | Generated | Keep the value |
S3_BUCKET_NAME | Bucket that holds all uploaded files, created on the first start | vulnsy | No. Files already uploaded stay in the old bucket. |
S3_ENDPOINT | S3 API URL that the app uses, on the internal Docker network | http://minio:9000 | Only when moving to another store |
S3_PUBLIC_ENDPOINT | S3 API URL in the signed download links that browsers open: the files host, at its root | https://${FILES_HOST} | Follows FILES_HOST |
S3_FORCE_PATH_STYLE | Puts the bucket name in the URL path rather than in the hostname, as MinIO requires | true | Keep true |
AWS_REGION | Region sent to the S3 API | us-east-1, MinIO's default | Only for another store |
AWS_ACCESS_KEY_ID | S3 access key that the app uses | ${MINIO_ROOT_USER} | Only for another store |
AWS_SECRET_ACCESS_KEY | S3 secret key that the app uses | ${MINIO_ROOT_PASSWORD} | Only for another store |
Storage Options
Bundled object storage (the default). The bundled image is Silo, a maintained MinIO-compatible fork (the MINIO_* variable names above are kept for familiarity). Files are kept in the vulnsy_minio-data Docker volume. The app connects to MinIO over the internal network (S3_ENDPOINT), and signs download links for the files host (S3_PUBLIC_ENDPOINT), which browsers can reach. The files host must be a plain reverse proxy of MinIO, at the root of its own hostname, that passes the Host header through unchanged: a signature covers the hostname and the path, so rewriting either one breaks every link.
Another S3-compatible store. To keep files in an S3-compatible service that you already run:
- Create a bucket and credentials for Vulnsy in that store.
- Set
S3_ENDPOINTto the store's S3 API URL as the server reaches it, andS3_PUBLIC_ENDPOINTto the URL at which your users' browsers reach it. The two are often the same. - Set
S3_BUCKET_NAME,AWS_REGION,AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYfor that store. - Run
docker compose up -d.
Vulnsy uses path-style requests, with the bucket name in the URL path, so the store must support them. If you switch an existing installation, first copy every object from the old bucket to the new one, keeping the object names. The bundled MinIO still starts, because the app depends on it, but it no longer receives files.
Vulnsy does not request server-side encryption from an S3-compatible store. For encryption at rest, encrypt the server's disk or enable encryption in the store.
Email (SMTP)
| Variable | Meaning | Default | After install |
|---|---|---|---|
EMAIL_FROM | Sender address of the emails Vulnsy sends. Your SMTP server must accept it. | noreply@ followed by the app host | Yes |
EMAIL_FROM_NAME | Sender display name | Vulnsy | Yes |
SMTP_HOST | SMTP server. When it is empty, Vulnsy sends no email. | Empty | Yes |
SMTP_PORT | SMTP port | 587 | Yes |
SMTP_SECURE | true: TLS from the start of the connection (implicit TLS, usually port 465). false: a plain connection, upgraded with STARTTLS when the server offers it (ports 587 and 25). | false | Yes |
SMTP_USER | SMTP username. When it is empty, Vulnsy does not authenticate. | Empty | Yes |
SMTP_PASSWORD | SMTP password | Empty | Yes |
VULNSY_ABUSE_EMAIL | Abuse contact printed in the footer of emails sent to your clients | The first administrator's email address | Yes |
SMTP Options
| Mail server | SMTP_PORT | SMTP_SECURE | SMTP_USER and SMTP_PASSWORD |
|---|---|---|---|
| Submission with STARTTLS | 587 | false | Your account |
| Implicit TLS | 465 | true | Your account |
| Internal relay that accepts mail from this server without authentication | 25, or the relay's port | false | Leave both empty |
Vulnsy sends account emails through this server: sign-in details for new users and temporary passwords. Client portal and disclosure emails use it as well, unless an administrator sets up a different SMTP server in the app under Admin > Email > Delivery. That server then takes precedence for those emails.
The app's log shows the email settings in use, in a line such as:
Email: SMTP smtp.example.com:587 (STARTTLS if offered, authenticated), from vulnsy@example.comFirst Administrator
| Variable | Meaning | Default | After install |
|---|---|---|---|
INITIAL_ADMIN_EMAIL | Email address of the first administrator | Set by the script | No effect after the first start |
INITIAL_ADMIN_PASSWORD | Initial password of the first administrator, which must be changed at first sign-in | Generated | No effect after the first start. You can remove it once you have signed in. |
Vulnsy reads these only on the first start, while the database is still empty. Two optional variables are read at the same time. They are included (left blank) in .env.self-hosted.example, so copy the value into .env before the first start if you want them. generate-env.sh does not set these two, so add them by hand:
| Variable | Meaning | Default | After install |
|---|---|---|---|
INITIAL_ADMIN_NAME | Display name of the first administrator | Administrator | No effect after the first start |
SELF_HOSTED_COMPANY_NAME | Name of the organization created on the first start | Vulnsy | No effect after the first start |
License
| Variable | Meaning | Default | After install |
|---|---|---|---|
VULNSY_LICENSE | The license token | Empty | Yes, see Licensing |
VULNSY_LICENSE_FILE | Path, inside the app container, to a file that holds the token. Mount the file into the container. Commented out in the example file. | Not set | Yes |
Authentication
| Variable | Meaning | Default | After install |
|---|---|---|---|
PASSWORD_BREACH_CHECK | Set to false to disable the Have I Been Pwned breached-password check. That check is the only outbound network call in the sign-in and password-change path (it sends a 5-character SHA-1 prefix using k-anonymity, never the password). Turn it off for air-gapped installs or where outbound calls are not allowed. | Enabled | Yes, restart the app |
AI
| Variable | Meaning | Default | After install |
|---|---|---|---|
OPENROUTER_API_KEY | Optional, and normally left empty. The AI modes of the self-hosted edition use the OpenRouter key or endpoint that an administrator enters in the app. | Empty | Yes |
OPENROUTER_BASE_URL | OpenRouter API URL used by the Your OpenRouter key mode. Commented out in the example file. | https://openrouter.ai/api/v1 | Yes |
AI_QUALITY_MODEL | Model for finding drafts, executive summaries and composed text in the Your OpenRouter key mode, when the administrator has not chosen one. Commented out in the example file. | Vulnsy's default | Yes |
AI_ECONOMY_MODEL | Model for rewrites and connection tests in the same mode, when the administrator has not chosen one. Commented out in the example file. | Vulnsy's default | Yes |
AI_ALLOW_PRIVATE_ENDPOINTS | true lets the Your own / local AI endpoint mode use plain HTTP, any port, and private network addresses | false | Yes |
AI Options
The AI assistant must be included in your license. An administrator chooses how it runs under Admin > Organization > AI Assistant. Vulnsy-managed AI is not available in the self-hosted edition, which leaves two modes:
- Your OpenRouter key. Requests go to OpenRouter with your own API key, so the server needs outbound HTTPS access to OpenRouter.
- Your own / local AI endpoint. Any OpenAI-compatible API, such as Ollama or vLLM. Vulnsy calls
/chat/completionsunder the base URL you enter.
By default, an endpoint must use HTTPS on port 443 or 8443, at a public address. For a model server on your own network, set AI_ALLOW_PRIVATE_ENDPOINTS=true and run docker compose up -d app. Vulnsy then also accepts http://, any port, and private addresses, including 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 and IPv6 unique-local addresses.
Loopback and link-local addresses are always blocked, including localhost, 127.0.0.1 and the cloud metadata address 169.254.169.254. Inside the app container, localhost is the container itself. Instead, use:
- The server's LAN address, when the model server runs on the Docker host, for example
http://192.168.1.20:11434/v1. The model server must listen on that address, not only on127.0.0.1. - A service name, when the model server runs as another service in the same Compose project, for example
http://ollama:11434/v1.
See Provider Modes for how the modes work in the app.