This commit is contained in:
parent
26d00abb80
commit
453c1241f7
14 changed files with 472 additions and 36 deletions
14
.dockerignore
Normal file
14
.dockerignore
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Everything the image does not need. Keeping the context small matters: the
|
||||
# build sends it to the daemon before doing anything.
|
||||
.git
|
||||
.github
|
||||
.forgejo
|
||||
target
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
dev-db
|
||||
/config.yml
|
||||
/config.yaml
|
||||
*.log
|
||||
README.md
|
||||
rename.sh
|
||||
2
.env
Normal file
2
.env
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# This file is used by dbmate, and by the sqlx macros at compile time.
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/app_template?sslmode=disable
|
||||
54
.forgejo/workflows/build.yml
Normal file
54
.forgejo/workflows/build.yml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# One job on purpose. The checks (eslint, vue-tsc, cargo test) live in the
|
||||
# Dockerfile stages, so this workflow needs a single job image — one that has
|
||||
# docker, git and node. Splitting the checks into their own jobs would mean
|
||||
# `container: rust:...`, and a JavaScript action like `actions/checkout` needs
|
||||
# node inside the job container, which the rust image does not have.
|
||||
#
|
||||
# Deliberately no third-party actions beyond `checkout`: a self-hosted Forgejo
|
||||
# runner resolves `uses:` against its own mirror, and `docker/*` is usually not
|
||||
# there. Plain docker commands work everywhere.
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
IMAGE: registry.tibiscuit.ch/vm-selector
|
||||
|
||||
jobs:
|
||||
image:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Secrets are read into the environment here, not interpolated into the
|
||||
# shell commands below: `${{ secrets.X }}` inside a `run:` ends up in the
|
||||
# process arguments, and the `secrets` context is not usable in `if:`.
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
PORTAINER_WEBHOOK: ${{ secrets.PORTAINER_WEBHOOK }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Turns itself on as soon as the registry has authentication and the two
|
||||
# secrets exist; skipped while the registry is open.
|
||||
- name: Log in to the registry
|
||||
if: env.REGISTRY_USERNAME != ''
|
||||
run: echo "$REGISTRY_PASSWORD" | docker login registry.tibiscuit.ch -u "$REGISTRY_USERNAME" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
run: |
|
||||
set -eu
|
||||
docker build \
|
||||
--build-arg GIT_HASH="${{ github.sha }}" \
|
||||
--tag "$IMAGE:${{ github.sha }}" \
|
||||
--tag "$IMAGE:latest" \
|
||||
.
|
||||
docker push "$IMAGE:${{ github.sha }}"
|
||||
docker push "$IMAGE:latest"
|
||||
|
||||
# Portainer re-pulls the image and recreates the stack. Enable
|
||||
# "Re-pull image" on the webhook, otherwise :latest stays the old layer.
|
||||
- name: Redeploy on Portainer
|
||||
if: env.PORTAINER_WEBHOOK != ''
|
||||
run: curl -fsS -X POST "$PORTAINER_WEBHOOK"
|
||||
32
.gitignore
vendored
32
.gitignore
vendored
|
|
@ -1,3 +1,31 @@
|
|||
# Build output
|
||||
/target
|
||||
config.yml
|
||||
config.yaml
|
||||
/frontend/dist
|
||||
|
||||
# Local configuration: this is where the secrets go, config.example.yml documents it.
|
||||
# Anchored to the root on purpose: an unanchored `config.yml` would also match
|
||||
# docker/config.yml, which is baked into the image and must stay committed.
|
||||
/config.yml
|
||||
/config.yaml
|
||||
|
||||
# Development database volume (dev-db/docker-compose.yml)
|
||||
/dev-db/db/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Editor and OS
|
||||
.idea/
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.DS_Store
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# NOT ignored, and must stay committed:
|
||||
# .sqlx/ sqlx offline data, so the image builds without a database
|
||||
# frontend/package-lock.json reproducible frontend builds in CI
|
||||
# db/schema.sql regenerated by dbmate, reviewed like any other change
|
||||
|
|
|
|||
44
.sqlx/query-19ecb5253ac44df6b974947efb48914080ad65aef67fbd4b7c5a354a972a9be8.json
generated
Normal file
44
.sqlx/query-19ecb5253ac44df6b974947efb48914080ad65aef67fbd4b7c5a354a972a9be8.json
generated
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, \"domain\", display_name, icon, \"position\"\n FROM vms\n ORDER BY \"position\", id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "domain",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "display_name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "icon",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "position",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "19ecb5253ac44df6b974947efb48914080ad65aef67fbd4b7c5a354a972a9be8"
|
||||
}
|
||||
46
.sqlx/query-5dd485ffb73dba5cc76cfb6eead8c40731f6d441ca25bd8f1688c9f1eece7547.json
generated
Normal file
46
.sqlx/query-5dd485ffb73dba5cc76cfb6eead8c40731f6d441ca25bd8f1688c9f1eece7547.json
generated
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, \"domain\", display_name, icon, \"position\"\n FROM vms\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "domain",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "display_name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "icon",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "position",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5dd485ffb73dba5cc76cfb6eead8c40731f6d441ca25bd8f1688c9f1eece7547"
|
||||
}
|
||||
73
Dockerfile
Normal file
73
Dockerfile
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Frontend: built first, it is the part that changes most often
|
||||
FROM node:22-slim AS frontend
|
||||
WORKDIR /build
|
||||
|
||||
# Dependencies in their own layer, so a source-only change does not reinstall them
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY frontend/ ./
|
||||
# The checks run here rather than in a separate CI job: the build then fails on
|
||||
# the runner for the same reason it would fail locally, and there is only one
|
||||
# environment to keep working.
|
||||
RUN npx eslint . --max-warnings=0
|
||||
RUN npm run build # type-check + vite build
|
||||
|
||||
|
||||
# Backend: compiled against the sqlx offline data, so no database is needed here
|
||||
FROM rust:1.97-slim AS backend
|
||||
WORKDIR /build
|
||||
|
||||
ENV SQLX_OFFLINE=true
|
||||
# Traceability: `build.rs` falls back on this when there is no git repository
|
||||
ARG GIT_HASH=unknown
|
||||
ENV GIT_HASH=${GIT_HASH}
|
||||
|
||||
# Warm the dependency layer with a dummy main, so editing src/ does not rebuild
|
||||
# every crate we depend on
|
||||
COPY Cargo.toml Cargo.lock build.rs ./
|
||||
RUN mkdir src && echo 'fn main() {}' > src/main.rs \
|
||||
&& cargo build --release \
|
||||
&& rm -rf src
|
||||
|
||||
COPY .sqlx/ .sqlx/
|
||||
COPY src/ src/
|
||||
# cargo skips a rebuild when only mtime changed: force it for our own crate
|
||||
RUN touch src/main.rs \
|
||||
&& cargo build --release --locked \
|
||||
&& cargo test --release --locked
|
||||
|
||||
|
||||
# Runtime
|
||||
FROM debian:trixie-slim
|
||||
LABEL org.opencontainers.image.title="vm-selector"
|
||||
|
||||
# virsh is how the app talks to libvirt. openssh-client is only needed if you
|
||||
# point vm.uri at a qemu+ssh:// uri instead of the mounted socket; curl serves
|
||||
# the healthcheck.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libvirt-clients openssh-client curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# The migrations travel with the image, so the stack needs nothing from the
|
||||
# repository: the `migrate` service runs this same image with another entrypoint.
|
||||
COPY --from=ghcr.io/amacneil/dbmate:2 /usr/local/bin/dbmate /usr/local/bin/dbmate
|
||||
COPY db/migrations/ /app/db/migrations/
|
||||
|
||||
# Unprivileged, but it must land in the host's libvirt group to reach the
|
||||
# socket: `group_add` in the compose file does that at run time.
|
||||
RUN useradd --system --create-home --uid 10001 app
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=backend /build/target/release/app-template /usr/local/bin/vm-selector
|
||||
COPY --from=frontend /build/dist/ /app/frontend/
|
||||
COPY docker/config.yml /etc/app-template/config.yml
|
||||
|
||||
USER app
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD curl -fsS http://localhost:3000/api/version || exit 1
|
||||
|
||||
# No shell: signals reach the binary directly, so the graceful shutdown works
|
||||
ENTRYPOINT ["/usr/local/bin/vm-selector"]
|
||||
99
README.md
99
README.md
|
|
@ -21,8 +21,7 @@ is the only thing that changes between the two.
|
|||
# 1. Start the development database
|
||||
cd dev-db && docker compose up -d && cd ..
|
||||
psql -h localhost -U postgres -c 'CREATE DATABASE app_template'
|
||||
dbmate up
|
||||
psql "$(grep DATABASE_URL .env | cut -d= -f2-)" -f db/seed.sql
|
||||
dbmate up # creates the schema and inserts the two VMs
|
||||
|
||||
# 2. Configure the app
|
||||
cp config.example.yml config.yml # then set vm.uri, see below
|
||||
|
|
@ -71,9 +70,8 @@ build dependency, and the uri alone decides local or remote. It needs `virsh` on
|
|||
│ │ └── hypervisor/ libvirt (virsh) and mock implementations
|
||||
│ └── utils/ configuration
|
||||
├── db/
|
||||
│ ├── migrations/ dbmate migrations
|
||||
│ ├── schema.sql dump regenerated by dbmate, do not edit by hand
|
||||
│ └── seed.sql the two VMs
|
||||
│ ├── migrations/ dbmate migrations (the two VMs are inserted by one)
|
||||
│ └── schema.sql dump regenerated by dbmate, do not edit by hand
|
||||
├── dev-db/ postgres + adminer for development
|
||||
└── frontend/
|
||||
└── src/
|
||||
|
|
@ -156,14 +154,89 @@ Rules of thumb:
|
|||
- texts live in `locales/*.yml` and are used through `$t('key')`, never hardcoded.
|
||||
- add a shadcn-vue component with `npx shadcn-vue@latest add <name>`.
|
||||
|
||||
## Production
|
||||
## Deployment
|
||||
|
||||
`cargo build --release`, `npm run build`, then point `frontend_dir` at the built
|
||||
`frontend/dist`: the backend serves the static files and falls back on `index.html` so
|
||||
the vue router keeps working on a page reload. Only one process to deploy.
|
||||
One image holds everything: the binary, the built frontend, the migrations and a
|
||||
`dbmate` to apply them. The backend serves the static files and falls back on
|
||||
`index.html`, so the vue router survives a page reload — a single container to run.
|
||||
|
||||
In the container, two things are needed on top of the binary:
|
||||
```
|
||||
push on main
|
||||
-> Forgejo Actions (.forgejo/workflows/build.yml)
|
||||
backend + frontend checks
|
||||
docker build --build-arg GIT_HASH=<sha>
|
||||
push registry.tibiscuit.ch/vm-selector:<sha> and :latest
|
||||
POST the Portainer webhook
|
||||
-> Portainer re-pulls and recreates the stack (docker-compose.yml)
|
||||
db -> migrate (runs dbmate up, exits) -> app
|
||||
```
|
||||
|
||||
- `virsh` installed, and `/var/run/libvirt/libvirt-sock` bind-mounted from the host
|
||||
- the container user in the host's `libvirt` group (`group_add: ["<gid>"]` in the
|
||||
compose file) — without it the socket is visible but every call is denied
|
||||
### The image builds without a database
|
||||
|
||||
`cargo sqlx prepare` writes `.sqlx/`, and it is **committed**. The macros check the
|
||||
sql against that data instead of a live server (`SQLX_OFFLINE=true`). Change a query
|
||||
and you must re-run it, or CI fails:
|
||||
|
||||
```bash
|
||||
cargo sqlx prepare # dev database must be up and migrated
|
||||
```
|
||||
|
||||
### Portainer stack
|
||||
|
||||
Deploy `docker-compose.yml` and set the four stack variables it documents at the top
|
||||
(`IMAGE`, `POSTGRES_PASSWORD`, `LIBVIRT_GID`, `APP_PORT`). Get the group id from the
|
||||
host:
|
||||
|
||||
```bash
|
||||
getent group libvirt | cut -d: -f3
|
||||
```
|
||||
|
||||
Then create a webhook on the stack, **tick "Re-pull image"**, and put its url in the
|
||||
`PORTAINER_WEBHOOK` secret of the repository. Without the re-pull, the webhook
|
||||
recreates the containers from the layer already on disk and nothing changes.
|
||||
|
||||
The two things that make libvirt reachable from inside the container:
|
||||
|
||||
- `/var/run/libvirt` bind-mounted from the host — the whole directory, since the
|
||||
socket is `libvirt-sock` or `virtqemud-sock` depending on the host's setup
|
||||
- the container in the host's `libvirt` group (`group_add`) — without it the socket
|
||||
is visible and every call is denied
|
||||
|
||||
### Domain name
|
||||
|
||||
The reverse proxy already in front of the registry handles it: add a proxy host for
|
||||
`vm.tibiscuit.ch` pointing at `192.168.0.104:${APP_PORT}`, and request a
|
||||
Let's Encrypt certificate. One manual step, once.
|
||||
|
||||
Making it *automatic* for every future service means letting the proxy read the
|
||||
container labels, which is what Traefik does and Nginx Proxy Manager does not — a
|
||||
worthwhile change, but a migration of the whole proxy, not something this stack can
|
||||
do on its own.
|
||||
|
||||
### Before exposing any of this
|
||||
|
||||
**The registry has no authentication.** `registry.tibiscuit.ch` is public, and
|
||||
`REGISTRY_STORAGE_DELETE_ENABLED` is on: anyone who finds it can pull every image,
|
||||
overwrite `vm-selector:latest`, or delete it — and Portainer would then deploy
|
||||
whatever they pushed. Put an htpasswd in front of it before the domain is indexed:
|
||||
|
||||
```yaml
|
||||
# in the registry stack
|
||||
environment:
|
||||
REGISTRY_AUTH: htpasswd
|
||||
REGISTRY_AUTH_HTPASSWD_REALM: registry
|
||||
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
|
||||
volumes:
|
||||
- ./auth:/auth
|
||||
```
|
||||
|
||||
```bash
|
||||
docker run --rm --entrypoint htpasswd httpd:2 -Bbn <user> <password> > auth/htpasswd
|
||||
```
|
||||
|
||||
Then set `REGISTRY_USERNAME` / `REGISTRY_PASSWORD` as repository secrets: the login
|
||||
step in the workflow turns itself on as soon as they exist.
|
||||
|
||||
**And the app itself has no authentication** — anyone who reaches `vm.tibiscuit.ch`
|
||||
can power your VMs on and off. Keep it off the public internet, or put access control
|
||||
in front of it.
|
||||
|
|
|
|||
10
build.rs
10
build.rs
|
|
@ -2,16 +2,24 @@ use std::process::Command;
|
|||
|
||||
/// Exposes the current commit and the crate name to the code through `env!()`
|
||||
fn main() {
|
||||
let git_hash = Command::new("git")
|
||||
// The docker build has no git repository: CI passes the commit in instead
|
||||
// (`--build-arg GIT_HASH=...`), so the running image stays traceable.
|
||||
let git_hash = std::env::var("GIT_HASH")
|
||||
.ok()
|
||||
.filter(|hash| !hash.is_empty() && hash != "unknown")
|
||||
.or_else(|| {
|
||||
Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|output| output.status.success())
|
||||
.and_then(|output| String::from_utf8(output.stdout).ok())
|
||||
})
|
||||
.map(|hash| hash.trim().chars().take(8).collect::<String>())
|
||||
.unwrap_or_else(|| "unknown".to_owned());
|
||||
|
||||
println!("cargo:rustc-env=GIT_HASH={git_hash}");
|
||||
println!("cargo:rerun-if-env-changed=GIT_HASH");
|
||||
println!(
|
||||
"cargo:rustc-env=CRATE_NAME={}",
|
||||
env!("CARGO_PKG_NAME").replace("-", "_")
|
||||
|
|
|
|||
13
db/migrations/20260729180000_add_vms.sql
Normal file
13
db/migrations/20260729180000_add_vms.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-- migrate:up
|
||||
-- The VMs are configuration, not demo data: without these rows the page has
|
||||
-- nothing to show. They belong to the schema so that a fresh deployment is
|
||||
-- usable straight after `dbmate up`, with no manual seeding step.
|
||||
--
|
||||
-- `domain` must match the libvirt domain name exactly (`virsh list --all`).
|
||||
INSERT INTO vms ("domain", "display_name", "icon", "position") VALUES
|
||||
('win11', 'Windows 11', 'windows', 1),
|
||||
('arch-hyprland', 'Arch Linux (Hyprland)', 'linux', 2)
|
||||
ON CONFLICT ("domain") DO NOTHING;
|
||||
|
||||
-- migrate:down
|
||||
DELETE FROM vms WHERE "domain" IN ('win11', 'arch-hyprland');
|
||||
|
|
@ -104,4 +104,5 @@ ALTER TABLE ONLY public.vms
|
|||
--
|
||||
|
||||
INSERT INTO public.schema_migrations (version) VALUES
|
||||
('20260729120000');
|
||||
('20260729120000'),
|
||||
('20260729180000');
|
||||
|
|
|
|||
15
db/seed.sql
15
db/seed.sql
|
|
@ -1,15 +0,0 @@
|
|||
-- Development data. Apply with:
|
||||
-- psql "$DATABASE_URL" -f db/seed.sql
|
||||
--
|
||||
-- `domain` must match the libvirt domain name exactly (`virsh list --all`).
|
||||
BEGIN;
|
||||
|
||||
INSERT INTO public.vms (id, "domain", "display_name", "icon", "position") VALUES
|
||||
(1, 'win11', 'Windows 11', 'windows', 1),
|
||||
(2, 'arch-hyprland', 'Arch Linux (Hyprland)', 'linux', 2)
|
||||
ON CONFLICT ("domain") DO NOTHING;
|
||||
|
||||
-- Keep the sequence in sync with the explicit ids inserted above
|
||||
SELECT setval('public.vms_id_seq', (SELECT COALESCE(MAX(id), 1) FROM public.vms));
|
||||
|
||||
COMMIT;
|
||||
72
docker-compose.yml
Normal file
72
docker-compose.yml
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Stack for Portainer. Everything it needs lives in the image, so it can be
|
||||
# deployed by pasting this file — no checkout of the repository on the server.
|
||||
#
|
||||
# Environment variables to set in the Portainer stack:
|
||||
# IMAGE registry.tibiscuit.ch/vm-selector:latest
|
||||
# POSTGRES_PASSWORD anything, it never leaves the internal network
|
||||
# LIBVIRT_GID the host's libvirt group id, from:
|
||||
# getent group libvirt | cut -d: -f3
|
||||
# APP_PORT host port for the reverse proxy to point at (default 3010)
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:18.3-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in the stack}
|
||||
POSTGRES_DB: vmselector
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres -d vmselector']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
networks: [internal]
|
||||
|
||||
# Runs once per deployment, before the app: applies any new migration, then exits
|
||||
migrate:
|
||||
image: ${IMAGE:?set IMAGE in the stack}
|
||||
restart: 'no'
|
||||
entrypoint:
|
||||
['dbmate', '--migrations-dir', '/app/db/migrations', '--no-dump-schema', 'up']
|
||||
environment:
|
||||
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/vmselector?sslmode=disable
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks: [internal]
|
||||
|
||||
app:
|
||||
image: ${IMAGE:?set IMAGE in the stack}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '${APP_PORT:-3010}:3000'
|
||||
environment:
|
||||
APP__POSTGRES__HOST: db
|
||||
APP__POSTGRES__PASSWORD: ${POSTGRES_PASSWORD}
|
||||
APP__POSTGRES__NAME: vmselector
|
||||
APP__VM__DRIVER: libvirt
|
||||
# The socket bind-mounted below, not ssh: the app runs on the same host
|
||||
APP__VM__URI: qemu:///system
|
||||
volumes:
|
||||
# The whole directory, not a single socket: depending on whether the host
|
||||
# runs the monolithic libvirtd or the split daemons, the app needs
|
||||
# libvirt-sock or virtqemud-sock.
|
||||
- /var/run/libvirt:/var/run/libvirt
|
||||
# Reaching that socket requires being in the host's libvirt group. Without
|
||||
# this the socket is visible and every call is denied.
|
||||
group_add:
|
||||
- '${LIBVIRT_GID:?see the header of this file}'
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
networks: [internal]
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
|
||||
networks:
|
||||
internal:
|
||||
23
docker/config.yml
Normal file
23
docker/config.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Baked into the image at /etc/app-template/config.yml: the defaults for a
|
||||
# containerised run. Every value is overridden by the environment
|
||||
# (APP__POSTGRES__PASSWORD=..., APP__VM__URI=...), which is how the compose file
|
||||
# passes the secrets — never edit this file to put one in.
|
||||
server:
|
||||
address: 0.0.0.0
|
||||
port: 3000
|
||||
|
||||
postgres:
|
||||
host: db
|
||||
port: 5432
|
||||
user: postgres
|
||||
password: postgres
|
||||
name: vmselector
|
||||
|
||||
vm:
|
||||
driver: libvirt
|
||||
# The host's libvirt socket, bind-mounted by the compose file
|
||||
uri: qemu:///system
|
||||
timeout_seconds: 20
|
||||
|
||||
# Where the Dockerfile puts the built frontend
|
||||
frontend_dir: /app/frontend
|
||||
Loading…
Add table
Reference in a new issue